You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
80 lines
2.6 KiB
TypeScript
80 lines
2.6 KiB
TypeScript
import type { OrderedExcalidrawElement } from "../element/types";
|
|
import { orderByFractionalIndex, syncInvalidIndices } 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.editingElement?.id ||
|
|
local.id === localAppState.resizingElement?.id ||
|
|
local.id === localAppState.draggingElement?.id || // TODO: Is this still valid? As draggingElement 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;
|
|
};
|
|
|
|
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);
|
|
|
|
// de-duplicate indices
|
|
syncInvalidIndices(orderedElements);
|
|
|
|
return orderedElements as ReconciledExcalidrawElement[];
|
|
};
|