Merge pull request #928 from excalidraw/fix_multiplayer_concurrency

Fix multiplayer concurrency
This commit is contained in:
Pete Hunt 2020-03-12 21:32:33 -07:00 committed by GitHub
commit f393486eed
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 55 additions and 19 deletions

View File

@ -291,7 +291,7 @@ export class App extends React.Component<any, AppState> {
} else { } else {
// create a map of ids so we don't have to iterate // create a map of ids so we don't have to iterate
// over the array more than once. // over the array more than once.
const elementMap = elements.reduce( const localElementMap = elements.reduce(
( (
acc: { [key: string]: ExcalidrawElement }, acc: { [key: string]: ExcalidrawElement },
element: ExcalidrawElement, element: ExcalidrawElement,
@ -302,15 +302,42 @@ export class App extends React.Component<any, AppState> {
{}, {},
); );
// Reconcile // Reconcile
elements = restoredState.elements.map(element => { elements = restoredState.elements
.reduce((elements, element) => {
// if the remote element references one that's currently
// edited on local, skip it (it'll be added in the next
// step)
if ( if (
elementMap.hasOwnProperty(element.id) && element.id === this.state.editingElement?.id ||
elementMap[element.id].version > element.version element.id === this.state.resizingElement?.id ||
element.id === this.state.draggingElement?.id
) { ) {
return elementMap[element.id]; return elements;
} }
return element;
}); if (
localElementMap.hasOwnProperty(element.id) &&
localElementMap[element.id].version > element.version
) {
elements.push(localElementMap[element.id]);
} else {
elements.push(element);
}
return elements;
}, [] as any)
// add local elements that are currently being edited
// (can't be done in the step above because the elements may
// not exist on remote at all)
.concat(
elements.filter(element => {
return (
element.id === this.state.editingElement?.id ||
element.id === this.state.resizingElement?.id ||
element.id === this.state.draggingElement?.id
);
}),
);
} }
this.setState({}); this.setState({});
if (this.socketInitialized === false) { if (this.socketInitialized === false) {
@ -358,7 +385,9 @@ export class App extends React.Component<any, AppState> {
this.broadcastSocketData({ this.broadcastSocketData({
type: "SCENE_UPDATE", type: "SCENE_UPDATE",
payload: { payload: {
elements, elements: elements.filter(element => {
return element.id !== this.state.editingElement?.id;
}),
appState: this.state, appState: this.state,
}, },
}); });
@ -1382,6 +1411,7 @@ export class App extends React.Component<any, AppState> {
elements = [...elements, element]; elements = [...elements, element];
this.setState({ this.setState({
draggingElement: element, draggingElement: element,
editingElement: element,
}); });
} }
} else if (element.type === "selection") { } else if (element.type === "selection") {
@ -1391,7 +1421,11 @@ export class App extends React.Component<any, AppState> {
}); });
} else { } else {
elements = [...elements, element]; elements = [...elements, element];
this.setState({ multiElement: null, draggingElement: element }); this.setState({
multiElement: null,
draggingElement: element,
editingElement: element,
});
} }
let resizeArrowFn: let resizeArrowFn:
@ -1860,6 +1894,7 @@ export class App extends React.Component<any, AppState> {
isResizing: false, isResizing: false,
resizingElement: null, resizingElement: null,
selectionElement: null, selectionElement: null,
editingElement: multiElement ? this.state.editingElement : null,
}); });
resizeArrowFn = null; resizeArrowFn = null;
@ -1883,7 +1918,10 @@ export class App extends React.Component<any, AppState> {
y - draggingElement.y, y - draggingElement.y,
]); ]);
invalidateShapeForElement(draggingElement); invalidateShapeForElement(draggingElement);
this.setState({ multiElement: this.state.draggingElement }); this.setState({
multiElement: this.state.draggingElement,
editingElement: this.state.draggingElement,
});
} else if (draggingOccurred && !multiElement) { } else if (draggingOccurred && !multiElement) {
if (!elementLocked) { if (!elementLocked) {
resetCursor(); resetCursor();
@ -2151,7 +2189,9 @@ export class App extends React.Component<any, AppState> {
this.broadcastSocketData({ this.broadcastSocketData({
type: "SCENE_UPDATE", type: "SCENE_UPDATE",
payload: { payload: {
elements, elements: elements.filter(element => {
return element.id !== this.state.editingElement?.id;
}),
appState: this.state, appState: this.state,
}, },
}); });

View File

@ -341,12 +341,10 @@ export async function exportCanvas(
export async function loadScene(id: string | null, privateKey?: string) { export async function loadScene(id: string | null, privateKey?: string) {
let data; let data;
let selectedId;
if (id != null) { if (id != null) {
// the private key is used to decrypt the content from the server, take // the private key is used to decrypt the content from the server, take
// extra care not to leak it // extra care not to leak it
data = await importFromBackend(id, privateKey); data = await importFromBackend(id, privateKey);
selectedId = id;
window.history.replaceState({}, "Excalidraw", window.location.origin); window.history.replaceState({}, "Excalidraw", window.location.origin);
} else { } else {
data = restoreFromLocalStorage(); data = restoreFromLocalStorage();
@ -354,6 +352,6 @@ export async function loadScene(id: string | null, privateKey?: string) {
return { return {
elements: data.elements, elements: data.elements,
appState: data.appState && { ...data.appState, selectedId }, appState: data.appState && { ...data.appState },
}; };
} }

View File

@ -7,5 +7,4 @@ export interface DataState {
source?: string; source?: string;
elements: readonly ExcalidrawElement[]; elements: readonly ExcalidrawElement[];
appState: AppState | null; appState: AppState | null;
selectedId?: number;
} }

View File

@ -28,7 +28,6 @@ export type AppState = {
cursorY: number; cursorY: number;
scrolledOutside: boolean; scrolledOutside: boolean;
name: string; name: string;
selectedId?: string;
isCollaborating: boolean; isCollaborating: boolean;
isResizing: boolean; isResizing: boolean;
zoom: number; zoom: number;