aboutsummaryrefslogtreecommitdiffstats
path: root/packages/excalidraw/data/reconcile.ts
diff options
context:
space:
mode:
authorkj_sh6042026-03-15 16:19:35 -0400
committerkj_sh6042026-03-15 16:19:35 -0400
commit6ec259a0e71174651bae95d4628138bf6fd68742 (patch)
tree5e33c6a5ec091ecabfcb257fdc7b6a88ed8754ac /packages/excalidraw/data/reconcile.ts
parent16c8578b15c727f22921f8a80a56ee4d4e7f2272 (diff)
refactor: packages/
Diffstat (limited to 'packages/excalidraw/data/reconcile.ts')
-rw-r--r--packages/excalidraw/data/reconcile.ts118
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[];
+};