|
@@ -6,6 +6,9 @@ import type {
|
|
|
OrderedExcalidrawElement,
|
|
|
} from "./element/types";
|
|
|
import { InvalidFractionalIndexError } from "./errors";
|
|
|
+import { hasBoundTextElement } from "./element/typeChecks";
|
|
|
+import { getBoundTextElement } from "./element/textElement";
|
|
|
+import { arrayToMap } from "./utils";
|
|
|
|
|
|
/**
|
|
|
* Envisioned relation between array order and fractional indices:
|
|
@@ -30,16 +33,79 @@ import { InvalidFractionalIndexError } from "./errors";
|
|
|
* @throws `InvalidFractionalIndexError` if invalid index is detected.
|
|
|
*/
|
|
|
export const validateFractionalIndices = (
|
|
|
- indices: (ExcalidrawElement["index"] | undefined)[],
|
|
|
+ elements: readonly ExcalidrawElement[],
|
|
|
+ {
|
|
|
+ shouldThrow = false,
|
|
|
+ includeBoundTextValidation = false,
|
|
|
+ reconciliationContext,
|
|
|
+ }: {
|
|
|
+ shouldThrow: boolean;
|
|
|
+ includeBoundTextValidation: boolean;
|
|
|
+ reconciliationContext?: {
|
|
|
+ localElements: ReadonlyArray<ExcalidrawElement>;
|
|
|
+ remoteElements: ReadonlyArray<ExcalidrawElement>;
|
|
|
+ };
|
|
|
+ },
|
|
|
) => {
|
|
|
+ const errorMessages = [];
|
|
|
+ const stringifyElement = (element: ExcalidrawElement | void) =>
|
|
|
+ `${element?.index}:${element?.id}:${element?.type}:${element?.isDeleted}:${element?.version}:${element?.versionNonce}`;
|
|
|
+
|
|
|
+ const indices = elements.map((x) => x.index);
|
|
|
for (const [i, index] of indices.entries()) {
|
|
|
const predecessorIndex = indices[i - 1];
|
|
|
const successorIndex = indices[i + 1];
|
|
|
|
|
|
if (!isValidFractionalIndex(index, predecessorIndex, successorIndex)) {
|
|
|
- throw new InvalidFractionalIndexError(
|
|
|
- `Fractional indices invariant for element has been compromised - ["${predecessorIndex}", "${index}", "${successorIndex}"] [predecessor, current, successor]`,
|
|
|
+ errorMessages.push(
|
|
|
+ `Fractional indices invariant has been compromised: "${stringifyElement(
|
|
|
+ elements[i - 1],
|
|
|
+ )}", "${stringifyElement(elements[i])}", "${stringifyElement(
|
|
|
+ elements[i + 1],
|
|
|
+ )}"`,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // disabled by default, as we don't fix it
|
|
|
+ if (includeBoundTextValidation && hasBoundTextElement(elements[i])) {
|
|
|
+ const container = elements[i];
|
|
|
+ const text = getBoundTextElement(container, arrayToMap(elements));
|
|
|
+
|
|
|
+ if (text && text.index! <= container.index!) {
|
|
|
+ errorMessages.push(
|
|
|
+ `Fractional indices invariant for bound elements has been compromised: "${stringifyElement(
|
|
|
+ text,
|
|
|
+ )}", "${stringifyElement(container)}"`,
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (errorMessages.length) {
|
|
|
+ const error = new InvalidFractionalIndexError();
|
|
|
+ const additionalContext = [];
|
|
|
+
|
|
|
+ if (reconciliationContext) {
|
|
|
+ additionalContext.push("Additional reconciliation context:");
|
|
|
+ additionalContext.push(
|
|
|
+ reconciliationContext.localElements.map((x) => stringifyElement(x)),
|
|
|
);
|
|
|
+ additionalContext.push(
|
|
|
+ reconciliationContext.remoteElements.map((x) => stringifyElement(x)),
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 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
|
|
|
+ throw error;
|
|
|
}
|
|
|
}
|
|
|
};
|
|
@@ -83,10 +149,15 @@ export const syncMovedIndices = (
|
|
|
|
|
|
// try generatating indices, throws on invalid movedElements
|
|
|
const elementsUpdates = generateIndices(elements, indicesGroups);
|
|
|
+ const elementsCandidates = elements.map((x) =>
|
|
|
+ elementsUpdates.has(x) ? { ...x, ...elementsUpdates.get(x) } : x,
|
|
|
+ );
|
|
|
|
|
|
// ensure next indices are valid before mutation, throws on invalid ones
|
|
|
validateFractionalIndices(
|
|
|
- elements.map((x) => elementsUpdates.get(x)?.index || x.index),
|
|
|
+ elementsCandidates,
|
|
|
+ // we don't autofix invalid bound text indices, hence don't include it in the validation
|
|
|
+ { includeBoundTextValidation: false, shouldThrow: true },
|
|
|
);
|
|
|
|
|
|
// split mutation so we don't end up in an incosistent state
|