Add version and versionNonce into delta

This commit is contained in:
Marcel Mraz 2025-05-23 14:01:08 +02:00
parent 14d512f321
commit bc1a71e772
No known key found for this signature in database
GPG Key ID: 4EBD6E62DC830CD2
3 changed files with 125 additions and 56 deletions

View File

@ -18,7 +18,12 @@ import type {
SceneElementsMap, SceneElementsMap,
} from "@excalidraw/element/types"; } from "@excalidraw/element/types";
import type { DTO, SubtypeOf, ValueOf } from "@excalidraw/common/utility-types"; import type {
DTO,
Mutable,
SubtypeOf,
ValueOf,
} from "@excalidraw/common/utility-types";
import type { import type {
AppState, AppState,
@ -51,6 +56,8 @@ import { orderByFractionalIndex, syncMovedIndices } from "./fractionalIndex";
import { Scene } from "./Scene"; import { Scene } from "./Scene";
import { StoreSnapshot } from "./store";
import type { BindableProp, BindingProp } from "./binding"; import type { BindableProp, BindingProp } from "./binding";
import type { ElementUpdate } from "./mutateElement"; import type { ElementUpdate } from "./mutateElement";
@ -858,10 +865,17 @@ export class AppStateDelta implements DeltaContainer<AppState> {
} }
} }
type ElementPartial<T extends ExcalidrawElement = ExcalidrawElement> = Omit< type ElementPartial<TElement extends ExcalidrawElement = ExcalidrawElement> =
ElementUpdate<Ordered<T>>, Omit<Partial<Ordered<TElement>>, "id" | "updated" | "seed">;
"seed"
>; export type ApplyToOptions = {
excludedProperties: Set<keyof ElementPartial>;
};
type ApplyToFlags = {
containsVisibleDifference: boolean;
containsZindexDifference: boolean;
};
/** /**
* Elements change is a low level primitive to capture a change between two sets of elements. * Elements change is a low level primitive to capture a change between two sets of elements.
@ -1101,8 +1115,11 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
for (const key of Object.keys(partial) as Array<keyof typeof partial>) { for (const key of Object.keys(partial) as Array<keyof typeof partial>) {
// do not update following props: // do not update following props:
// - `version` and `versionNonce`, as they should keep it's original value
// - `boundElements`, as it is a reference value which is postprocessed to contain only deleted/inserted keys // - `boundElements`, as it is a reference value which is postprocessed to contain only deleted/inserted keys
switch (key) { switch (key) {
case "version":
case "versionNonce":
case "boundElements": case "boundElements":
latestPartial[key] = partial[key]; latestPartial[key] = partial[key];
break; break;
@ -1150,12 +1167,15 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
public applyTo( public applyTo(
elements: SceneElementsMap, elements: SceneElementsMap,
elementsSnapshot: Map<string, OrderedExcalidrawElement>, snapshot: StoreSnapshot["elements"] = StoreSnapshot.empty().elements,
options: ApplyToOptions = {
excludedProperties: new Set(),
},
): [SceneElementsMap, boolean] { ): [SceneElementsMap, boolean] {
let nextElements = new Map(elements) as SceneElementsMap; let nextElements = new Map(elements) as SceneElementsMap;
let changedElements: Map<string, OrderedExcalidrawElement>; let changedElements: Map<string, OrderedExcalidrawElement>;
const flags = { const flags: ApplyToFlags = {
containsVisibleDifference: false, containsVisibleDifference: false,
containsZindexDifference: false, containsZindexDifference: false,
}; };
@ -1164,13 +1184,14 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
try { try {
const applyDeltas = ElementsDelta.createApplier( const applyDeltas = ElementsDelta.createApplier(
nextElements, nextElements,
elementsSnapshot, snapshot,
options,
flags, flags,
); );
const addedElements = applyDeltas("added", this.added); const addedElements = applyDeltas(this.added);
const removedElements = applyDeltas("removed", this.removed); const removedElements = applyDeltas(this.removed);
const updatedElements = applyDeltas("updated", this.updated); const updatedElements = applyDeltas(this.updated);
const affectedElements = this.resolveConflicts(elements, nextElements); const affectedElements = this.resolveConflicts(elements, nextElements);
@ -1229,18 +1250,12 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
private static createApplier = private static createApplier =
( (
nextElements: SceneElementsMap, nextElements: SceneElementsMap,
snapshot: Map<string, OrderedExcalidrawElement>, snapshot: StoreSnapshot["elements"],
flags: { options: ApplyToOptions,
containsVisibleDifference: boolean; flags: ApplyToFlags,
containsZindexDifference: boolean;
},
) => ) =>
( (deltas: Record<string, Delta<ElementPartial>>) => {
type: "added" | "removed" | "updated",
deltas: Record<string, Delta<ElementPartial>>,
) => {
const getElement = ElementsDelta.createGetter( const getElement = ElementsDelta.createGetter(
type,
nextElements, nextElements,
snapshot, snapshot,
flags, flags,
@ -1250,7 +1265,13 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
const element = getElement(id, delta.inserted); const element = getElement(id, delta.inserted);
if (element) { if (element) {
const newElement = ElementsDelta.applyDelta(element, delta, flags); const newElement = ElementsDelta.applyDelta(
element,
delta,
options,
flags,
);
nextElements.set(newElement.id, newElement); nextElements.set(newElement.id, newElement);
acc.set(newElement.id, newElement); acc.set(newElement.id, newElement);
} }
@ -1261,13 +1282,9 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
private static createGetter = private static createGetter =
( (
type: "added" | "removed" | "updated",
elements: SceneElementsMap, elements: SceneElementsMap,
snapshot: Map<string, OrderedExcalidrawElement>, snapshot: StoreSnapshot["elements"],
flags: { flags: ApplyToFlags,
containsVisibleDifference: boolean;
containsZindexDifference: boolean;
},
) => ) =>
(id: string, partial: ElementPartial) => { (id: string, partial: ElementPartial) => {
let element = elements.get(id); let element = elements.get(id);
@ -1281,10 +1298,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
flags.containsZindexDifference = true; flags.containsZindexDifference = true;
// as the element was force deleted, we need to check if adding it back results in a visible change // as the element was force deleted, we need to check if adding it back results in a visible change
if ( if (!partial.isDeleted || (partial.isDeleted && !element.isDeleted)) {
partial.isDeleted === false ||
(partial.isDeleted !== true && element.isDeleted === false)
) {
flags.containsVisibleDifference = true; flags.containsVisibleDifference = true;
} }
} else { } else {
@ -1304,16 +1318,25 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
private static applyDelta( private static applyDelta(
element: OrderedExcalidrawElement, element: OrderedExcalidrawElement,
delta: Delta<ElementPartial>, delta: Delta<ElementPartial>,
flags: { options: ApplyToOptions,
containsVisibleDifference: boolean; flags: ApplyToFlags,
containsZindexDifference: boolean;
} = {
// by default we don't care about about the flags
containsVisibleDifference: true,
containsZindexDifference: true,
},
) { ) {
const { boundElements, ...directlyApplicablePartial } = delta.inserted; const directlyApplicablePartial: Mutable<ElementPartial> = {};
for (const key of Object.keys(delta.inserted) as Array<
keyof typeof delta.inserted
>) {
if (key === "boundElements") {
continue;
}
if (options.excludedProperties.has(key)) {
continue;
}
const value = delta.inserted[key];
Reflect.set(directlyApplicablePartial, key, value);
}
if ( if (
delta.deleted.boundElements?.length || delta.deleted.boundElements?.length ||
@ -1665,7 +1688,7 @@ export class ElementsDelta implements DeltaContainer<SceneElementsMap> {
private static stripIrrelevantProps( private static stripIrrelevantProps(
partial: Partial<OrderedExcalidrawElement>, partial: Partial<OrderedExcalidrawElement>,
): ElementPartial { ): ElementPartial {
const { id, updated, version, versionNonce, ...strippedPartial } = partial; const { id, updated, ...strippedPartial } = partial;
return strippedPartial; return strippedPartial;
} }

View File

@ -534,7 +534,7 @@ export class StoreDelta {
/** /**
* Inverse store delta, creates new instance of `StoreDelta`. * Inverse store delta, creates new instance of `StoreDelta`.
*/ */
public static inverse(delta: StoreDelta): StoreDelta { public static inverse(delta: StoreDelta) {
return this.create(delta.elements.inverse(), delta.appState.inverse()); return this.create(delta.elements.inverse(), delta.appState.inverse());
} }
@ -545,7 +545,7 @@ export class StoreDelta {
delta: StoreDelta, delta: StoreDelta,
elements: SceneElementsMap, elements: SceneElementsMap,
modifierOptions: "deleted" | "inserted", modifierOptions: "deleted" | "inserted",
): StoreDelta { ) {
return this.create( return this.create(
delta.elements.applyLatestChanges(elements, modifierOptions), delta.elements.applyLatestChanges(elements, modifierOptions),
delta.appState, delta.appState,
@ -562,12 +562,9 @@ export class StoreDelta {
delta: StoreDelta, delta: StoreDelta,
elements: SceneElementsMap, elements: SceneElementsMap,
appState: AppState, appState: AppState,
prevSnapshot: StoreSnapshot = StoreSnapshot.empty(),
): [SceneElementsMap, AppState, boolean] { ): [SceneElementsMap, AppState, boolean] {
const [nextElements, elementsContainVisibleChange] = delta.elements.applyTo( const [nextElements, elementsContainVisibleChange] =
elements, delta.elements.applyTo(elements);
prevSnapshot.elements,
);
const [nextAppState, appStateContainsVisibleChange] = const [nextAppState, appStateContainsVisibleChange] =
delta.appState.applyTo(appState, nextElements); delta.appState.applyTo(appState, nextElements);

View File

@ -7,11 +7,65 @@ import {
type Store, type Store,
} from "@excalidraw/element"; } from "@excalidraw/element";
import type { StoreSnapshot } from "@excalidraw/element";
import type { SceneElementsMap } from "@excalidraw/element/types"; import type { SceneElementsMap } from "@excalidraw/element/types";
import type { AppState } from "./types"; import type { AppState } from "./types";
class HistoryEntry extends StoreDelta {} class HistoryEntry extends StoreDelta {
/**
* Apply the delta to the passed elements and appState, does not modify the snapshot.
*/
public applyTo(
elements: SceneElementsMap,
appState: AppState,
snapshot: StoreSnapshot,
): [SceneElementsMap, AppState, boolean] {
const [nextElements, elementsContainVisibleChange] = this.elements.applyTo(
elements,
// used to fallback into local snapshot in case we couldn't apply the delta
// due to a missing elements in the scene (force deleted)
snapshot.elements,
// we don't want to apply the version and versionNonce properties for history
{
excludedProperties: new Set(["version", "versionNonce"]),
},
);
const [nextAppState, appStateContainsVisibleChange] = this.appState.applyTo(
appState,
nextElements,
);
const appliedVisibleChanges =
elementsContainVisibleChange || appStateContainsVisibleChange;
return [nextElements, nextAppState, appliedVisibleChanges];
}
/**
* Overriding once to avoid type casting everywhere.
*/
public static override inverse(delta: StoreDelta): HistoryEntry {
return super.inverse(delta) as HistoryEntry;
}
/**
* Overriding once to avoid type casting everywhere.
*/
public static override applyLatestChanges(
delta: StoreDelta,
elements: SceneElementsMap,
modifierOptions: "deleted" | "inserted",
): HistoryEntry {
return super.applyLatestChanges(
delta,
elements,
modifierOptions,
) as HistoryEntry;
}
}
export class HistoryChangedEvent { export class HistoryChangedEvent {
constructor( constructor(
@ -112,12 +166,7 @@ export class History {
while (historyEntry) { while (historyEntry) {
try { try {
[nextElements, nextAppState, containsVisibleChange] = [nextElements, nextAppState, containsVisibleChange] =
StoreDelta.applyTo( historyEntry.applyTo(nextElements, nextAppState, prevSnapshot);
historyEntry,
nextElements,
nextAppState,
prevSnapshot,
);
const nextSnapshot = prevSnapshot.maybeClone( const nextSnapshot = prevSnapshot.maybeClone(
action, action,