Expose store, a bit

mrazator/delta-based-sync
Marcel Mraz 2 months ago
parent 52eaf64591
commit 245d681b7d
No known key found for this signature in database
GPG Key ID: 4EBD6E62DC830CD2

@ -107,6 +107,7 @@ import Trans from "../packages/excalidraw/components/Trans";
import { ShareDialog, shareDialogStateAtom } from "./share/ShareDialog";
import CollabError, { collabErrorIndicatorAtom } from "./collab/CollabError";
import type { RemoteExcalidrawElement } from "../packages/excalidraw/data/reconcile";
import type { StoreIncrementEvent } from "../packages/excalidraw/store";
import {
CommandPalette,
DEFAULT_CATEGORIES,
@ -663,6 +664,17 @@ const ExcalidrawWrapper = () => {
}
};
const onIncrement = (increment: StoreIncrementEvent) => {
// ephemerals are not part of this (which is alright)
// - wysiwyg, dragging elements / points, mouse movements, etc.
const { elementsChange } = increment;
// some appState like selections should also be transfered (we could even persist it)
if (!elementsChange.isEmpty()) {
console.log(elementsChange)
}
};
const [latestShareableLink, setLatestShareableLink] = useState<string | null>(
null,
);
@ -795,6 +807,7 @@ const ExcalidrawWrapper = () => {
<Excalidraw
excalidrawAPI={excalidrawRefCallback}
onChange={onChange}
onIncrement={onIncrement}
initialData={initialStatePromiseRef.current.promise}
isCollaborating={isCollaborating}
onPointerUpdate={collabAPI?.onPointerUpdate}

@ -806,33 +806,37 @@ type ElementPartial<T extends ExcalidrawElement = ExcalidrawElement> = Omit<
*/
export class ElementsChange implements Change<SceneElementsMap> {
private constructor(
private readonly added: Map<string, Delta<ElementPartial>>,
private readonly removed: Map<string, Delta<ElementPartial>>,
private readonly updated: Map<string, Delta<ElementPartial>>,
private readonly added: Record<string, Delta<ElementPartial>>,
private readonly removed: Record<string, Delta<ElementPartial>>,
private readonly updated: Record<string, Delta<ElementPartial>>,
) {}
public static create(
added: Map<string, Delta<ElementPartial>>,
removed: Map<string, Delta<ElementPartial>>,
updated: Map<string, Delta<ElementPartial>>,
added: Record<string, Delta<ElementPartial>>,
removed: Record<string, Delta<ElementPartial>>,
updated: Record<string, Delta<ElementPartial>>,
options = { shouldRedistribute: false },
) {
let change: ElementsChange;
if (options.shouldRedistribute) {
const nextAdded = new Map<string, Delta<ElementPartial>>();
const nextRemoved = new Map<string, Delta<ElementPartial>>();
const nextUpdated = new Map<string, Delta<ElementPartial>>();
const nextAdded: Record<string, Delta<ElementPartial>> = {};
const nextRemoved: Record<string, Delta<ElementPartial>> = {};
const nextUpdated: Record<string, Delta<ElementPartial>> = {};
const deltas = [...added, ...removed, ...updated];
const deltas = [
...Object.entries(added),
...Object.entries(removed),
...Object.entries(updated),
];
for (const [id, delta] of deltas) {
if (this.satisfiesAddition(delta)) {
nextAdded.set(id, delta);
nextAdded[id] = delta;
} else if (this.satisfiesRemoval(delta)) {
nextRemoved.set(id, delta);
nextRemoved[id] = delta;
} else {
nextUpdated.set(id, delta);
nextUpdated[id] = delta;
}
}
@ -873,7 +877,7 @@ export class ElementsChange implements Change<SceneElementsMap> {
type: "added" | "removed" | "updated",
satifies: (delta: Delta<ElementPartial>) => boolean,
) {
for (const [id, delta] of change[type].entries()) {
for (const [id, delta] of Object.entries(change[type])) {
if (!satifies(delta)) {
console.error(
`Broken invariant for "${type}" delta, element "${id}", delta:`,
@ -900,9 +904,9 @@ export class ElementsChange implements Change<SceneElementsMap> {
return ElementsChange.empty();
}
const added = new Map<string, Delta<ElementPartial>>();
const removed = new Map<string, Delta<ElementPartial>>();
const updated = new Map<string, Delta<ElementPartial>>();
const added: Record<string, Delta<ElementPartial>> = {};
const removed: Record<string, Delta<ElementPartial>> = {};
const updated: Record<string, Delta<ElementPartial>> = {};
// this might be needed only in same edge cases, like during collab, when `isDeleted` elements get removed or when we (un)intentionally remove the elements
for (const prevElement of prevElements.values()) {
@ -918,7 +922,7 @@ export class ElementsChange implements Change<SceneElementsMap> {
ElementsChange.stripIrrelevantProps,
);
removed.set(prevElement.id, delta);
removed[prevElement.id] = delta;
}
}
@ -938,7 +942,7 @@ export class ElementsChange implements Change<SceneElementsMap> {
ElementsChange.stripIrrelevantProps,
);
added.set(nextElement.id, delta);
added[nextElement.id] = delta;
continue;
}
@ -959,9 +963,9 @@ export class ElementsChange implements Change<SceneElementsMap> {
) {
// notice that other props could have been updated as well
if (prevElement.isDeleted && !nextElement.isDeleted) {
added.set(nextElement.id, delta);
added[nextElement.id] = delta;
} else {
removed.set(nextElement.id, delta);
removed[nextElement.id] = delta;
}
continue;
@ -969,7 +973,7 @@ export class ElementsChange implements Change<SceneElementsMap> {
// making sure there are at least some changes
if (!Delta.isEmpty(delta)) {
updated.set(nextElement.id, delta);
updated[nextElement.id] = delta;
}
}
}
@ -978,15 +982,23 @@ export class ElementsChange implements Change<SceneElementsMap> {
}
public static empty() {
return ElementsChange.create(new Map(), new Map(), new Map());
return ElementsChange.create({}, {}, {});
}
public static load(data: {
added: Record<string, Delta<ElementPartial>>;
removed: Record<string, Delta<ElementPartial>>;
updated: Record<string, Delta<ElementPartial>>;
}) {
return ElementsChange.create(data.added, data.removed, data.updated);
}
public inverse(): ElementsChange {
const inverseInternal = (deltas: Map<string, Delta<ElementPartial>>) => {
const inversedDeltas = new Map<string, Delta<ElementPartial>>();
const inverseInternal = (deltas: Record<string, Delta<ElementPartial>>) => {
const inversedDeltas: Record<string, Delta<ElementPartial>> = {};
for (const [id, delta] of deltas.entries()) {
inversedDeltas.set(id, Delta.create(delta.inserted, delta.deleted));
for (const [id, delta] of Object.entries(deltas)) {
inversedDeltas[id] = Delta.create(delta.inserted, delta.deleted);
}
return inversedDeltas;
@ -1002,9 +1014,9 @@ export class ElementsChange implements Change<SceneElementsMap> {
public isEmpty(): boolean {
return (
this.added.size === 0 &&
this.removed.size === 0 &&
this.updated.size === 0
Object.keys(this.added).length === 0 &&
Object.keys(this.removed).length === 0 &&
Object.keys(this.updated).length === 0
);
}
@ -1036,11 +1048,11 @@ export class ElementsChange implements Change<SceneElementsMap> {
};
const applyLatestChangesInternal = (
deltas: Map<string, Delta<ElementPartial>>,
deltas: Record<string, Delta<ElementPartial>>,
) => {
const modifiedDeltas = new Map<string, Delta<ElementPartial>>();
const modifiedDeltas: Record<string, Delta<ElementPartial>> = {};
for (const [id, delta] of deltas.entries()) {
for (const [id, delta] of Object.entries(deltas)) {
const existingElement = elements.get(id);
if (existingElement) {
@ -1051,9 +1063,9 @@ export class ElementsChange implements Change<SceneElementsMap> {
"inserted",
);
modifiedDeltas.set(id, modifiedDelta);
modifiedDeltas[id] = modifiedDelta;
} else {
modifiedDeltas.set(id, delta);
modifiedDeltas[id] = delta;
}
}
@ -1158,8 +1170,8 @@ export class ElementsChange implements Change<SceneElementsMap> {
flags,
);
return (deltas: Map<string, Delta<ElementPartial>>) =>
Array.from(deltas.entries()).reduce((acc, [id, delta]) => {
return (deltas: Record<string, Delta<ElementPartial>>) =>
Object.entries(deltas).reduce((acc, [id, delta]) => {
const element = getElement(id, delta.inserted);
if (element) {
@ -1331,20 +1343,21 @@ export class ElementsChange implements Change<SceneElementsMap> {
};
// removed delta is affecting the bindings always, as all the affected elements of the removed elements need to be unbound
for (const [id] of this.removed) {
for (const id of Object.keys(this.removed)) {
ElementsChange.unbindAffected(prevElements, nextElements, id, updater);
}
// added delta is affecting the bindings always, all the affected elements of the added elements need to be rebound
for (const [id] of this.added) {
for (const id of Object.keys(this.added)) {
ElementsChange.rebindAffected(prevElements, nextElements, id, updater);
}
// updated delta is affecting the binding only in case it contains changed binding or bindable property
for (const [id] of Array.from(this.updated).filter(([_, delta]) =>
Object.keys({ ...delta.deleted, ...delta.inserted }).find((prop) =>
bindingProperties.has(prop as BindingProp | BindableProp),
),
for (const [id] of Array.from(Object.entries(this.updated)).filter(
([_, delta]) =>
Object.keys({ ...delta.deleted, ...delta.inserted }).find((prop) =>
bindingProperties.has(prop as BindingProp | BindableProp),
),
)) {
const updatedElement = nextElements.get(id);
if (!updatedElement || updatedElement.isDeleted) {
@ -1367,16 +1380,16 @@ export class ElementsChange implements Change<SceneElementsMap> {
nextAffectedElements,
);
for (const [id, delta] of added) {
this.added.set(id, delta);
for (const [id, delta] of Object.entries(added)) {
this.added[id] = delta;
}
for (const [id, delta] of removed) {
this.removed.set(id, delta);
for (const [id, delta] of Object.entries(removed)) {
this.removed[id] = delta;
}
for (const [id, delta] of updated) {
this.updated.set(id, delta);
for (const [id, delta] of Object.entries(updated)) {
this.updated[id] = delta;
}
return nextAffectedElements;

@ -739,6 +739,7 @@ class App extends React.Component<AppProps, AppState> {
updateFrameRendering: this.updateFrameRendering,
toggleSidebar: this.toggleSidebar,
onChange: (cb) => this.onChangeEmitter.on(cb),
onIncrement: (cb) => this.store.onStoreIncrementEmitter.on(cb),
onPointerDown: (cb) => this.onPointerDownEmitter.on(cb),
onPointerUp: (cb) => this.onPointerUpEmitter.on(cb),
onScrollChange: (cb) => this.onScrollChangeEmitter.on(cb),
@ -2462,6 +2463,7 @@ class App extends React.Component<AppProps, AppState> {
this.store.onStoreIncrementEmitter.on((increment) => {
this.history.record(increment.elementsChange, increment.appStateChange);
this.props.onIncrement?.(increment);
});
this.scene.onUpdate(this.triggerRender);

@ -22,6 +22,7 @@ polyfill();
const ExcalidrawBase = (props: ExcalidrawProps) => {
const {
onChange,
onIncrement,
initialData,
excalidrawAPI,
isCollaborating = false,
@ -111,6 +112,7 @@ const ExcalidrawBase = (props: ExcalidrawProps) => {
<InitializeApp langCode={langCode} theme={theme}>
<App
onChange={onChange}
onIncrement={onIncrement}
initialData={initialData}
excalidrawAPI={excalidrawAPI}
isCollaborating={isCollaborating}

@ -75,7 +75,7 @@ export type StoreActionType = ValueOf<typeof StoreAction>;
/**
* Represent an increment to the Store.
*/
class StoreIncrementEvent {
export class StoreIncrementEvent {
constructor(
public readonly elementsChange: ElementsChange,
public readonly appStateChange: AppStateChange,
@ -395,6 +395,7 @@ export class Snapshot {
!prev ||
!next ||
prev.id !== next.id ||
prev.version !== next.version ||
prev.versionNonce !== next.versionNonce
) {
return true;

@ -40,7 +40,7 @@ import type { IMAGE_MIME_TYPES, MIME_TYPES } from "./constants";
import type { ContextMenuItems } from "./components/ContextMenu";
import type { SnapLine } from "./snapping";
import type { Merge, MaybePromise, ValueOf, MakeBrand } from "./utility-types";
import type { StoreActionType } from "./store";
import type { StoreActionType, StoreIncrementEvent } from "./store";
export type SocketId = string & { _brand: "SocketId" };
@ -498,6 +498,7 @@ export interface ExcalidrawProps {
appState: AppState,
files: BinaryFiles,
) => void;
onIncrement?: (event: StoreIncrementEvent) => void;
initialData?:
| (() => MaybePromise<ExcalidrawInitialDataState | null>)
| MaybePromise<ExcalidrawInitialDataState | null>;
@ -782,6 +783,9 @@ export interface ExcalidrawImperativeAPI {
files: BinaryFiles,
) => void,
) => UnsubscribeCallback;
onIncrement: (
callback: (event: StoreIncrementEvent) => void,
) => UnsubscribeCallback;
onPointerDown: (
callback: (
activeTool: AppState["activeTool"],

Loading…
Cancel
Save