diff --git a/packages/excalidraw/data/reconcile.ts b/packages/excalidraw/data/reconcile.ts index 1df28e37ec..aa663ebfa7 100644 --- a/packages/excalidraw/data/reconcile.ts +++ b/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); diff --git a/packages/excalidraw/fractionalIndex.ts b/packages/excalidraw/fractionalIndex.ts index 7d54cff077..e594a1358a 100644 --- a/packages/excalidraw/fractionalIndex.ts +++ b/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; remoteElements: ReadonlyArray; @@ -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 diff --git a/packages/excalidraw/scene/Scene.ts b/packages/excalidraw/scene/Scene.ts index c237059c4f..813b3cbf53 100644 --- a/packages/excalidraw/scene/Scene.ts +++ b/packages/excalidraw/scene/Scene.ts @@ -1,3 +1,4 @@ +import throttle from "lodash.throttle"; import type { ExcalidrawElement, NonDeletedExcalidrawElement, @@ -50,6 +51,24 @@ const getNonDeletedElements = ( 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["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(); diff --git a/packages/excalidraw/tests/fractionalIndex.test.ts b/packages/excalidraw/tests/fractionalIndex.test.ts index 10dbc004b1..b57af016b1 100644 --- a/packages/excalidraw/tests/fractionalIndex.test.ts +++ b/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);