diff options
| author | kj_sh604 | 2026-03-15 16:19:35 -0400 |
|---|---|---|
| committer | kj_sh604 | 2026-03-15 16:19:35 -0400 |
| commit | 6ec259a0e71174651bae95d4628138bf6fd68742 (patch) | |
| tree | 5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/data/reconcile.ts | |
| parent | 16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff) | |
refactor: packages/
Diffstat (limited to 'packages/excalidraw/data/reconcile.ts')
| -rw-r--r-- | packages/excalidraw/data/reconcile.ts | 118 |
1 files changed, 118 insertions, 0 deletions
diff --git a/packages/excalidraw/data/reconcile.ts b/packages/excalidraw/data/reconcile.ts new file mode 100644 index 0000000..fa4cff8 --- /dev/null +++ b/packages/excalidraw/data/reconcile.ts @@ -0,0 +1,118 @@ +import throttle from "lodash.throttle"; +import { ENV } from "../constants"; +import type { OrderedExcalidrawElement } from "../element/types"; +import { + orderByFractionalIndex, + syncInvalidIndices, + validateFractionalIndices, +} from "../fractionalIndex"; +import type { AppState } from "../types"; +import type { MakeBrand } from "../utility-types"; +import { arrayToMap } from "../utils"; + +export type ReconciledExcalidrawElement = OrderedExcalidrawElement & + MakeBrand<"ReconciledElement">; + +export type RemoteExcalidrawElement = OrderedExcalidrawElement & + MakeBrand<"RemoteExcalidrawElement">; + +const shouldDiscardRemoteElement = ( + localAppState: AppState, + local: OrderedExcalidrawElement | undefined, + remote: RemoteExcalidrawElement, +): boolean => { + if ( + local && + // local element is being edited + (local.id === localAppState.editingTextElement?.id || + local.id === localAppState.resizingElement?.id || + local.id === localAppState.newElement?.id || // TODO: Is this still valid? As newElement is selection element, which is never part of the elements array + // local element is newer + local.version > remote.version || + // resolve conflicting edits deterministically by taking the one with + // the lowest versionNonce + (local.version === remote.version && + local.versionNonce < remote.versionNonce)) + ) { + return true; + } + 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[], + localAppState: AppState, +): ReconciledExcalidrawElement[] => { + const localElementsMap = arrayToMap(localElements); + const reconciledElements: OrderedExcalidrawElement[] = []; + const added = new Set<string>(); + + // process remote elements + for (const remoteElement of remoteElements) { + if (!added.has(remoteElement.id)) { + const localElement = localElementsMap.get(remoteElement.id); + const discardRemoteElement = shouldDiscardRemoteElement( + localAppState, + localElement, + remoteElement, + ); + + if (localElement && discardRemoteElement) { + reconciledElements.push(localElement); + added.add(localElement.id); + } else { + reconciledElements.push(remoteElement); + added.add(remoteElement.id); + } + } + } + + // process remaining local elements + for (const localElement of localElements) { + if (!added.has(localElement.id)) { + reconciledElements.push(localElement); + added.add(localElement.id); + } + } + + const orderedElements = orderByFractionalIndex(reconciledElements); + + validateIndicesThrottled(orderedElements, localElements, remoteElements); + + // de-duplicate indices + syncInvalidIndices(orderedElements); + + return orderedElements as ReconciledExcalidrawElement[]; +}; |
