浏览代码

fix: throttle fractional indices validation (#8306)

Marcel Mraz 11 月之前
父节点
当前提交
84d89b9a8a

+ 33 - 20
packages/excalidraw/data/reconcile.ts

@@ -1,3 +1,4 @@
+import throttle from "lodash.throttle";
 import { ENV } from "../constants";
 import type { OrderedExcalidrawElement } from "../element/types";
 import {
@@ -38,6 +39,37 @@ const shouldDiscardRemoteElement = (
   return false;
 };
 
+const validateIndicesThrottled = throttle(
+  (
+    orderedElements: readonly OrderedExcalidrawElement[],
+    localElements: readonly OrderedExcalidrawElement[],
+    remoteElements: readonly RemoteExcalidrawElement[],
+  ) => {
+    if (
+      import.meta.env.DEV ||
+      import.meta.env.MODE === ENV.TEST ||
+      window?.DEBUG_FRACTIONAL_INDICES
+    ) {
+      // create new instances due to the mutation
+      const elements = syncInvalidIndices(
+        orderedElements.map((x) => ({ ...x })),
+      );
+
+      validateFractionalIndices(elements, {
+        // throw in dev & test only, to remain functional on `DEBUG_FRACTIONAL_INDICES`
+        shouldThrow: import.meta.env.DEV || import.meta.env.MODE === ENV.TEST,
+        includeBoundTextValidation: true,
+        reconciliationContext: {
+          localElements,
+          remoteElements,
+        },
+      });
+    }
+  },
+  1000 * 60,
+  { leading: true, trailing: false },
+);
+
 export const reconcileElements = (
   localElements: readonly OrderedExcalidrawElement[],
   remoteElements: readonly RemoteExcalidrawElement[],
@@ -77,26 +109,7 @@ export const reconcileElements = (
 
   const orderedElements = orderByFractionalIndex(reconciledElements);
 
-  if (
-    import.meta.env.DEV ||
-    import.meta.env.MODE === ENV.TEST ||
-    window?.DEBUG_FRACTIONAL_INDICES
-  ) {
-    const elements = syncInvalidIndices(
-      // create new instances due to the mutation
-      orderedElements.map((x) => ({ ...x })),
-    );
-
-    validateFractionalIndices(elements, {
-      // throw in dev & test only, to remain functional on `DEBUG_FRACTIONAL_INDICES`
-      shouldThrow: import.meta.env.DEV || import.meta.env.MODE === ENV.TEST,
-      includeBoundTextValidation: true,
-      reconciliationContext: {
-        localElements,
-        remoteElements,
-      },
-    });
-  }
+  validateIndicesThrottled(orderedElements, localElements, remoteElements);
 
   // de-duplicate indices
   syncInvalidIndices(orderedElements);

+ 16 - 8
packages/excalidraw/fractionalIndex.ts

@@ -37,10 +37,12 @@ export const validateFractionalIndices = (
   {
     shouldThrow = false,
     includeBoundTextValidation = false,
+    ignoreLogs,
     reconciliationContext,
   }: {
     shouldThrow: boolean;
     includeBoundTextValidation: boolean;
+    ignoreLogs?: true;
     reconciliationContext?: {
       localElements: ReadonlyArray<ExcalidrawElement>;
       remoteElements: ReadonlyArray<ExcalidrawElement>;
@@ -95,13 +97,15 @@ export const validateFractionalIndices = (
       );
     }
 
-    // report just once and with the stacktrace
-    console.error(
-      errorMessages.join("\n\n"),
-      error.stack,
-      elements.map((x) => stringifyElement(x)),
-      ...additionalContext,
-    );
+    if (!ignoreLogs) {
+      // report just once and with the stacktrace
+      console.error(
+        errorMessages.join("\n\n"),
+        error.stack,
+        elements.map((x) => stringifyElement(x)),
+        ...additionalContext,
+      );
+    }
 
     if (shouldThrow) {
       // if enabled, gather all the errors first, throw once
@@ -157,7 +161,11 @@ export const syncMovedIndices = (
     validateFractionalIndices(
       elementsCandidates,
       // we don't autofix invalid bound text indices, hence don't include it in the validation
-      { includeBoundTextValidation: false, shouldThrow: true },
+      {
+        includeBoundTextValidation: false,
+        shouldThrow: true,
+        ignoreLogs: true,
+      },
     );
 
     // split mutation so we don't end up in an incosistent state

+ 20 - 12
packages/excalidraw/scene/Scene.ts

@@ -1,3 +1,4 @@
+import throttle from "lodash.throttle";
 import type {
   ExcalidrawElement,
   NonDeletedExcalidrawElement,
@@ -50,6 +51,24 @@ const getNonDeletedElements = <T extends ExcalidrawElement>(
   return { elementsMap, elements };
 };
 
+const validateIndicesThrottled = throttle(
+  (elements: readonly ExcalidrawElement[]) => {
+    if (
+      import.meta.env.DEV ||
+      import.meta.env.MODE === ENV.TEST ||
+      window?.DEBUG_FRACTIONAL_INDICES
+    ) {
+      validateFractionalIndices(elements, {
+        // throw only in dev & test, to remain functional on `DEBUG_FRACTIONAL_INDICES`
+        shouldThrow: import.meta.env.DEV || import.meta.env.MODE === ENV.TEST,
+        includeBoundTextValidation: true,
+      });
+    }
+  },
+  1000 * 60,
+  { leading: true, trailing: false },
+);
+
 const hashSelectionOpts = (
   opts: Parameters<InstanceType<typeof Scene>["getSelectedElements"]>[0],
 ) => {
@@ -274,18 +293,7 @@ class Scene {
         : Array.from(nextElements.values());
     const nextFrameLikes: ExcalidrawFrameLikeElement[] = [];
 
-    if (
-      import.meta.env.DEV ||
-      import.meta.env.MODE === ENV.TEST ||
-      window?.DEBUG_FRACTIONAL_INDICES
-    ) {
-      validateFractionalIndices(_nextElements, {
-        // validate everything
-        includeBoundTextValidation: true,
-        // throw only in dev & test, to remain functional on `DEBUG_FRACTIONAL_INDICES`
-        shouldThrow: import.meta.env.DEV || import.meta.env.MODE === ENV.TEST,
-      });
-    }
+    validateIndicesThrottled(_nextElements);
 
     this.elements = syncInvalidIndices(_nextElements);
     this.elementsMap.clear();

+ 2 - 0
packages/excalidraw/tests/fractionalIndex.test.ts

@@ -766,6 +766,7 @@ function test(
         validateFractionalIndices(elements, {
           shouldThrow: true,
           includeBoundTextValidation: true,
+          ignoreLogs: true,
         }),
       ).toThrowError(InvalidFractionalIndexError);
     }
@@ -783,6 +784,7 @@ function test(
       validateFractionalIndices(syncedElements, {
         shouldThrow: true,
         includeBoundTextValidation: true,
+        ignoreLogs: true,
       }),
     ).not.toThrowError(InvalidFractionalIndexError);