1
0
mirror of https://github.com/haiwen/seahub.git synced 2025-09-25 14:50:29 +00:00

Optimize exdraw module 4 (#8129)

* add sync module

* optimize code

* optimize saveImage module

* optimize code

---------

Co-authored-by: 小强 <shuntian@Mac.lan>
This commit is contained in:
杨顺强
2025-08-13 16:20:34 +08:00
committed by GitHub
parent ee7e96842a
commit 1bc04bf347
5 changed files with 651 additions and 47 deletions

View File

@@ -41,7 +41,7 @@ class FileManager {
if (fileData && !this.isFileSavedOrBeingSaved(fileData)) {
addedFiles.set(element.fileId, files[element.fileId]);
this.savedFiles.set(element.fileId, this.getFileVersion(fileData));
this.savingFiles.set(element.fileId, this.getFileVersion(fileData));
}
}
@@ -98,9 +98,10 @@ class FileManager {
};
shouldPreventUnload = (elements) => {
return elements.some(element => {
const hasUnsavedImage = elements.some(element => {
return isInitializedImageElement(element) && !element.isDeleted && this.savingFiles.has(element.fileId);
});
return hasUnsavedImage;
};
shouldUpdateImageElementStatus = (element) => {

View File

@@ -1,14 +1,17 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { CaptureUpdateAction, Excalidraw, MainMenu, newElementWith, reconcileElements, restore, restoreElements, useHandleLibrary } from '@excalidraw/excalidraw';
import { CaptureUpdateAction, Excalidraw, MainMenu, newElementWith, reconcileElements, restoreElements, useHandleLibrary } from '@excalidraw/excalidraw';
import { langList } from '../constants';
import { LibraryIndexedDBAdapter } from './library-adapter';
import Collab from '../collaboration/collab';
import context from '../context';
import TipMessage from './tip-message';
import { importFromLocalStorage } from '../data/local-storage';
import { resolvablePromise, updateStaleImageStatuses } from '../utils/exdraw-utils';
import { getFilename, isInitializedImageElement } from '../utils/element-utils';
import LocalData from '../data/local-data';
import SocketManager from '../socket/socket-manager';
import { loadFromServerStorage } from '../data/server-storage';
import { getSyncableElements } from '../data';
import { gettext } from '../../../utils/constants';
import '@excalidraw/excalidraw/index.css';
@@ -22,32 +25,26 @@ const UIOptions = {
tools: { image: true },
};
const initializeScene = async (collabAPI) => {
const initializeScene = async () => {
// load local data from localstorage
const docUuid = context.getDocUuid();
const localDataState = importFromLocalStorage(docUuid); // {appState, elements}
let data = null;
// load remote data from server
if (collabAPI) {
const scene = await collabAPI.startCollaboration();
const { elements } = scene;
const restoredRemoteElements = restoreElements(elements, null);
const reconciledElements = reconcileElements(
localDataState.elements,
restoredRemoteElements,
localDataState.appState,
);
data = {
elements: reconciledElements,
appState: localDataState.appState,
};
} else {
data = restore(localDataState || null, null, null, {
repairBindings: true,
});
}
const scene = await loadFromServerStorage();
const { elements } = scene;
const restoredRemoteElements = restoreElements(elements, null);
const reconciledElements = reconcileElements(
localDataState.elements,
restoredRemoteElements,
localDataState.appState,
);
data = {
elements: reconciledElements,
appState: null,
version: scene.version,
};
return {
scene: data
@@ -55,7 +52,7 @@ const initializeScene = async (collabAPI) => {
};
const SimpleEditor = () => {
const collabAPIRef = useRef(null);
const initialStatePromiseRef = useRef({ promise: null });
if (!initialStatePromiseRef.current.promise) {
initialStatePromiseRef.current.promise = resolvablePromise();
@@ -70,9 +67,10 @@ const SimpleEditor = () => {
const loadImages = (data, isInitialLoad) => {
if (!data.scene) return;
if (collabAPIRef.current && collabAPIRef.current.getIsServerConnected()) {
const socketManager = SocketManager.getInstance();
if (socketManager) {
if (data.scene.elements) {
collabAPIRef.current.fetchImageFilesFromServer({
socketManager.fetchImageFilesFromServer({
elements: data.scene.elements,
forceFetchFiles: true,
}).then(({ loadedFiles, erroredFiles }) => {
@@ -113,10 +111,9 @@ const SimpleEditor = () => {
context.initSettings().then(() => {
const config = context.getSettings();
const collabAPI = new Collab(excalidrawAPI, config);
collabAPIRef.current = collabAPI;
initializeScene(collabAPI).then(async (data) => {
initializeScene().then(async (data) => {
// init socket
SocketManager.getInstance(excalidrawAPI, data.scene, config);
loadImages(data, /* isInitialLoad */true);
initialStatePromiseRef.current.promise.resolve(data.scene);
});
@@ -125,9 +122,8 @@ const SimpleEditor = () => {
}, [excalidrawAPI]);
const handleChange = useCallback((elements, appState, files) => {
if (collabAPIRef.current) {
collabAPIRef.current.syncElements(elements);
}
const socketManager = SocketManager.getInstance();
socketManager.syncLocalElementsToOthers(elements);
const docUuid = context.getDocUuid();
if (!LocalData.isSavePaused()) {
@@ -158,20 +154,30 @@ const SimpleEditor = () => {
}, [excalidrawAPI]);
const handlePointerUpdate = useCallback((payload) => {
if (!collabAPIRef.current) return;
collabAPIRef.current.syncPointer(payload);
const socketManager = SocketManager.getInstance();
socketManager.syncMouseLocationToOthers(payload);
}, []);
const beforeUnload = useCallback((event) => {
const socketManager = SocketManager.getInstance();
const fileManager = socketManager.fileManager;
const elements = excalidrawAPI.getSceneElementsIncludingDeleted();
const syncableElements = getSyncableElements(elements);
if (fileManager.shouldPreventUnload(syncableElements)) {
// eslint-disable-next-line no-console
console.warn('The uploaded image has not been saved yet. Please close this page later.');
event.preventDefault();
event.returnValue = gettext('The uploaded image has not been saved yet. Please close this page later.');
}
return;
}, [excalidrawAPI]);
useEffect(() => {
const beforeUnload = (event) => {
// event.preventDefault();
LocalData.flushSave();
};
window.addEventListener('beforeunload', beforeUnload);
return () => {
window.removeEventListener('beforeunload', beforeUnload);
};
}, []);
}, [beforeUnload]);
return (
<div className='excali-container'>

View File

@@ -1,7 +1,8 @@
import React from 'react';
import { withTranslation } from 'react-i18next';
import dayjs from 'dayjs';
import EventBus from '../../../components/common/event-bus';
import EventBus from '../utils/event-bus';
import { gettext } from '../../../utils/constants';
import toaster from '../../../components/toast';
class TipMessage extends React.Component {
constructor(props) {
@@ -18,13 +19,95 @@ class TipMessage extends React.Component {
const eventBus = EventBus.getInstance();
this.unsubscribeSavingEvent = eventBus.subscribe('is-saving', this.onDocumentSaving);
this.unsubscribeSavedEvent = eventBus.subscribe('saved', this.onDocumentSaved);
// offline reconnect
this.unsubscribeDisconnectEvent = eventBus.subscribe('disconnect', this.onDisconnect);
this.unsubscribeReconnectErrorEvent = eventBus.subscribe('reconnect_error', this.onReconnectError);
this.unsubscribeReconnectEvent = eventBus.subscribe('reconnect', this.onReconnect);
// server return error
this.unsubscribeOpExecError = eventBus.subscribe('execute_client_operations_error', this.onOperationExecuteError);
this.unsubscribeSyncServerOpError = eventBus.subscribe('sync_server_operations_error', this.onSyncServerOperationError);
this.unsubscribeDocumentLoadError = eventBus.subscribe('load_document_content_error', this.onInternalServerExecError);
this.unsubscribeOperationsSaveError = eventBus.subscribe('token_expired', this.onTokenExpiredError);
// local error
this.unsubscribePendingOpExceedLimit = eventBus.subscribe('pending_operations_exceed_limit', this.onPendingOpExceedLimit);
}
componentWillUnmount() {
this.unsubscribeSavingEvent();
this.unsubscribeSavedEvent();
this.unsubscribeDisconnectEvent();
this.unsubscribeReconnectErrorEvent();
this.unsubscribeReconnectEvent();
this.unsubscribeOpExecError();
this.unsubscribeSyncServerOpError();
this.unsubscribePendingOpExceedLimit();
this.unsubscribeDocumentLoadError();
this.unsubscribeOperationsSaveError();
clearTimeout(this.saveTimer);
}
onOperationExecuteError = () => {
const copyright = 'Failed to execute operation on server, the current operation has been withdrawn';
const message = gettext(copyright);
toaster.warning(message, { hasCloseButton: true });
};
onSyncServerOperationError = () => {
const copyright = 'Synchronization with the server failed, please refresh the page';
const message = gettext(copyright);
toaster.danger(message, { hasCloseButton: false, duration: null });
};
onInternalServerExecError = () => {
const copyright = 'An exception occurred on the server, please refresh the page and try again';
const message = gettext(copyright);
toaster.danger(message, { hasCloseButton: false, duration: null });
};
onTokenExpiredError = (msg) => {
const copyright = 'Token expired. Please refresh the page.';
const message = gettext(copyright);
toaster.closeAll();
toaster.danger(message, { duration: null });
};
onPendingOpExceedLimit = () => {
toaster.closeAll();
const copyright = 'There are multiple operations not synced to the server. Please check your network.';
const message = gettext(copyright);
toaster.warning(message, { duration: 5 });
};
onDisconnect = () => {
const copyright = 'Server is not connected. Operation will be sent to server later.';
const message = gettext(copyright);
toaster.warning(message, { hasCloseButton: true, duration: null });
};
onReconnectError = () => {
if (!this.isConnectError) {
this.isConnectError = true;
const copyright = 'Server is disconnected. Reconnecting...';
const message = gettext(copyright);
toaster.closeAll();
toaster.warning(message, { hasCloseButton: true, duration: null });
}
};
onReconnect = () => {
this.isConnectError = false;
const copyright = 'Server is reconnected.';
const message = gettext(copyright);
toaster.closeAll();
toaster.success(message); // close after serval seconds
};
onDocumentSaving = () => {
this.setState({
isSaving: true,
@@ -51,15 +134,14 @@ class TipMessage extends React.Component {
};
render = () => {
const { t } = this.props;
const { isSaved, isSaving, lastSavedAt } = this.state;
if (isSaving && !isSaved) {
return <span className="tip-message">{t('Saving')}</span>;
return <span className="tip-message">{gettext('Saving...')}</span>;
}
if (!isSaving && isSaved) {
return <span className="tip-message">{t('All_changes_saved')}</span>;
return <span className="tip-message">{gettext('All changes saved')}</span>;
}
if (lastSavedAt) {
return (
@@ -74,4 +156,4 @@ class TipMessage extends React.Component {
};
}
export default withTranslation('sdoc-editor')(TipMessage);
export default TipMessage;

View File

@@ -0,0 +1,199 @@
import { CaptureUpdateAction, newElementWith } from '@excalidraw/excalidraw';
import io from 'socket.io-client';
import throttle from 'lodash.throttle';
import { isSyncableElement } from '../data';
import { clientDebug, serverDebug } from '../utils/debug';
import SocketManager from './socket-manager';
import { getFilename } from '../utils/element-utils';
import { FILE_UPLOAD_TIMEOUT } from '../constants';
class SocketClient {
constructor(config) {
this.config = config;
this.isReconnect = false;
this.broadcastedElementVersions = new Map();
this.socket = io(`${config.exdrawServer}/exdraw`, {
reconnection: true,
auth: { token: config.accessToken },
query: {
'doc_uuid': config.docUuid,
}
});
this.socket.on('connect', this.onConnected);
this.socket.on('disconnect', this.onDisconnected);
this.socket.on('connect_error', this.onConnectError);
this.socket.on('init-room', this.onInitRoom);
this.socket.on('room-user-change', this.onRoomUserChanged);
this.socket.on('leave-room', this.onLeaveRoom);
this.socket.on('elements-updated', this.onReceiveRemoteElementsUpdate);
this.socket.on('mouse-location-updated', this.onReceiveRemoteMouseLocationUpdate);
this.socket.io.on('reconnect', this.onReconnect);
this.socket.io.on('reconnect_attempt', this.onReconnectAttempt);
this.socket.io.on('reconnect_error', this.onReconnectError);
}
getParams = (payload = {}) => {
const { docUuid, user } = this.config;
return {
docUuid,
user,
...payload,
};
};
onConnected = () => {
if (this.isReconnect) {
this.isReconnect = false;
}
};
onDisconnected = (data) => {
if (data === 'ping timeout') {
clientDebug('Disconnected due to ping timeout, trying to reconnect...');
this.socket.connect();
return;
}
clientDebug('disconnect message: %s', data);
const socketManager = SocketManager.getInstance();
socketManager.dispatchConnectState('disconnect');
};
onConnectError = () => {
clientDebug('connect_error.');
const socketManager = SocketManager.getInstance();
socketManager.dispatchConnectState('connect_error');
};
queueFileUpload = throttle(async () => {
const socketManager = SocketManager.getInstance();
let savedFiles = new Map();
try {
({ savedFiles } = await socketManager.fileManager.saveFiles({
elements: socketManager.excalidrawAPI.getSceneElementsIncludingDeleted(),
files: socketManager.excalidrawAPI.getFiles()
}));
} catch (error) {
if (error.name !== 'AbortError') {
socketManager.excalidrawAPI.updateScene({
appState: {
errorMessage: error.message,
},
});
}
}
let isChanged = false;
const oldElements = socketManager.excalidrawAPI.getSceneElementsIncludingDeleted();
const newElements = oldElements.map(element => {
if (socketManager.fileManager.shouldUpdateImageElementStatus(element)) {
isChanged = true;
const fileData = savedFiles.get(element.fileId);
if (fileData) {
const filename = getFilename(element.fileId, fileData);
return newElementWith(element, { status: 'saved', filename });
}
return element;
}
return element;
});
if (isChanged) {
socketManager.excalidrawAPI.updateScene({
elements: newElements,
captureUpdate: CaptureUpdateAction.NEVER,
});
}
}, FILE_UPLOAD_TIMEOUT);
broadcastSceneElements = (elements, version, callback) => {
const syncableElements = elements.reduce((acc, element) => {
const isAddedOrUpdated = !this.broadcastedElementVersions.has(element.id) || element.version > this.broadcastedElementVersions.get(element.id);
if (isAddedOrUpdated && isSyncableElement(element)) {
acc.push(element);
}
return acc;
}, []);
for (const syncableElement of syncableElements) {
this.broadcastedElementVersions.set(
syncableElement.id,
syncableElement.version,
);
}
this.queueFileUpload();
const payload = {
elements: syncableElements,
version: version,
};
const params = this.getParams(payload);
this.socket.emit('elements-updated', params, (result) => {
callback && callback(result);
});
};
broadcastMouseLocation = (payload) => {
const params = this.getParams(payload);
this.socket.emit('mouse-location-updated', params);
};
onInitRoom = () => {
serverDebug('join-room message');
this.socket.emit('join-room', this.getParams());
};
onRoomUserChanged = (users) => {
serverDebug('room users changed. all users count: %s', users.length);
const socketManager = SocketManager.getInstance();
socketManager.receiveRoomUserChanged(users);
};
onLeaveRoom = (userInfo) => {
serverDebug('%s leaved room success.', userInfo.name);
const socketManager = SocketManager.getInstance();
socketManager.receiveLeaveRoom(userInfo);
};
onReceiveRemoteElementsUpdate = (params) => {
serverDebug('sync elements by another updated, %O', params);
const socketManager = SocketManager.getInstance();
socketManager.handleRemoteSceneUpdated(params);
};
onReceiveRemoteMouseLocationUpdate = (params) => {
serverDebug('sync another\'s mouse location, %O', params);
const socketManager = SocketManager.getInstance();
socketManager.handleRemoteMouseLocationUpdated(params);
};
onReconnect = () => {
clientDebug('reconnect.');
this.isReconnect = true;
const socketManager = SocketManager.getInstance();
socketManager.dispatchConnectState('reconnect');
};
onReconnectAttempt = (attemptNumber) => {
clientDebug('reconnect_attempt. %s', attemptNumber);
const socketManager = SocketManager.getInstance();
socketManager.dispatchConnectState('reconnect_attempt', attemptNumber);
};
onReconnectError = () => {
clientDebug('reconnect_error.');
const socketManager = SocketManager.getInstance();
socketManager.dispatchConnectState('reconnect_error');
};
close = () => {
this.socket.close();
};
}
export default SocketClient;

View File

@@ -0,0 +1,316 @@
import { stateDebug } from '../utils/debug';
import SocketClient from './socket-client';
import { CaptureUpdateAction, getSceneVersion, reconcileElements, restoreElements } from '@excalidraw/excalidraw';
import { CURSOR_SYNC_TIMEOUT, LOAD_IMAGES_TIMEOUT } from '../constants';
import throttle from 'lodash.throttle';
import EventBus from '../utils/event-bus';
import FileManager from '../data/file-manager';
import { loadFilesFromServer, saveFilesToServer } from '../data/server-storage';
import { updateStaleImageStatuses } from '../utils/exdraw-utils';
import { isInitializedImageElement } from '../utils/element-utils';
const STATE = {
IDLE: 'idle',
SENDING: 'sending',
CONFLICT: 'conflict',
DISCONNECT: 'disconnect',
NEED_RELOAD: 'need_reload',
};
class SocketManager {
constructor(excalidrawAPI, document, config) {
this.config = config;
this.document = document;
this.excalidrawAPI = excalidrawAPI;
this.state = STATE.IDLE;
this.pendingOperationList = [];
this.pendingOperationBeginTimeList = [];
this.collaborators = new Map();
const { user } = config;
this.collaborators.set(user._username, user, { isCurrentUser: true });
this.excalidrawAPI.updateScene({ collaborators: this.collaborators });
this.eventBus = EventBus.getInstance();
this.socketClient = new SocketClient(config);
this.lastBroadcastedOrReceivedSceneVersion = 0; // used check is need sync or not
if (document && document.elements) {
this.setLastBroadcastedOrReceivedSceneVersion(document.elements);
}
this.fileManager = new FileManager({
getFiles: async (ids) => {
return loadFilesFromServer(ids);
},
saveFiles: async ({ addedFiles }) => {
const { savedFiles, erroredFiles } = await saveFilesToServer(addedFiles);
return {
savedFiles: savedFiles.reduce((acc, id) => {
const fileData = addedFiles.get(id);
if (fileData) {
acc.set(id, fileData);
}
return acc;
}, new Map()),
erroredFiles: erroredFiles.reduce((acc, id) => {
const fileData = addedFiles.get(id);
if (fileData) {
acc.set(id, fileData);
}
return acc;
}, new Map())
};
}
});
}
static getInstance = (excalidrawAPI, document, socketConfig) => {
if (this.instance) {
return this.instance;
}
if (!excalidrawAPI || !document || !socketConfig) {
throw new Error('SocketManager init params is invalid. Place check your code to fix it.');
}
this.instance = new SocketManager(excalidrawAPI, document, socketConfig);
return this.instance;
};
getVersion = () => {
return this.document.version;
};
setVersion = (version) => {
this.document.version = version;
};
setLastBroadcastedOrReceivedSceneVersion = (elements) => {
const version = getSceneVersion(elements);
this.lastBroadcastedOrReceivedSceneVersion = version;
};
getLastBroadcastedOrReceivedSceneVersion = () => {
return this.lastBroadcastedOrReceivedSceneVersion;
};
fetchImageFilesFromServer = async (opts) => {
const fileIds = opts.elements.filter(element => {
return (
isInitializedImageElement(element) &&
!this.fileManager.isFileTracked(element.fileId) &&
!element.isDeleted &&
(opts.forceFetchFiles ? element.status !== 'pending' || Date.now() - element.updated > 10000 : element.status === 'saved')
);
}).map(element => {
return element.filename || element.fileId;
});
return await this.fileManager.getFiles(fileIds);
};
loadImageFiles = throttle(async () => {
const { loadedFiles, erroredFiles } =
await this.fetchImageFilesFromServer({
elements: this.excalidrawAPI.getSceneElementsIncludingDeleted(),
});
this.excalidrawAPI.addFiles(loadedFiles);
updateStaleImageStatuses({
excalidrawAPI: this.excalidrawAPI,
erroredFiles,
elements: this.excalidrawAPI.getSceneElementsIncludingDeleted(),
});
}, LOAD_IMAGES_TIMEOUT);
isNeedToSync = (elements) => {
const currentVersion = getSceneVersion(elements);
if (currentVersion > this.lastBroadcastedOrReceivedSceneVersion) {
return true;
}
return false;
};
syncLocalElementsToOthers = (elements) => {
if (!this.isNeedToSync(elements)) {
return;
}
this.pendingOperationList.push(elements);
const lastOpBeginTime = new Date().getTime();
this.pendingOperationBeginTimeList.push(lastOpBeginTime);
const firstOpBeginTime = this.pendingOperationBeginTimeList[0];
const isExceedExecuteTime = (lastOpBeginTime - firstOpBeginTime) / 1000 > 30 ? true : false;
if (isExceedExecuteTime || this.pendingOperationList.length > 50) {
this.dispatchConnectState('pending_operations_exceed_limit');
}
this.sendOperations();
};
sendOperations = () => {
if (this.state !== STATE.IDLE) return;
stateDebug(`State changed: ${this.state} -> ${STATE.SENDING}`);
this.state = STATE.SENDING;
this.sendNextOperations();
};
sendNextOperations = () => {
if (this.state !== STATE.SENDING) return;
if (this.pendingOperationList.length === 0) {
stateDebug(`State Changed: ${this.state} -> ${STATE.IDLE}`);
this.state = STATE.IDLE;
return;
}
this.dispatchConnectState('is-saving');
const version = this.document.version;
const elements = this.pendingOperationList.shift();
this._sendingOperation = elements;
this.socketClient.broadcastSceneElements(elements, version, this.sendOperationsCallback);
};
sendOperationsCallback = (result) => {
if (result && result.success) {
const { version: serverVersion } = result;
this.setVersion(serverVersion);
const lastSavedAt = new Date().getTime();
this.dispatchConnectState('saved', lastSavedAt);
this.setLastBroadcastedOrReceivedSceneVersion(this._sendingOperation);
// send next operations
this.pendingOperationBeginTimeList.shift(); // remove current operation's begin time
this._sendingOperation = null;
this.sendNextOperations();
return;
}
// Operations are execute failure
const { error_type } = result;
if (error_type === 'load_document_content_error' || error_type === 'token_expired') {
// load_document_content_error: After a short-term reconnection, the content of the document fails to load
this.dispatchConnectState(error_type);
// reset sending control
stateDebug(`State Changed: ${this.state} -> ${STATE.NEED_RELOAD}`);
this.state = STATE.NEED_RELOAD;
this._sendingOperation = null;
} else if (error_type === 'version_behind_server') {
// Put the failed operation into the pending list and re-execute it
this.pendingOperationList.unshift(this._sendingOperation);
stateDebug(`State Changed: ${this.state} -> ${STATE.CONFLICT}`);
this.state = STATE.CONFLICT;
this.resolveConflicting(result);
}
};
resolveConflicting = (result) => {
const { elements, version } = result;
this.updateLocalDataByRemoteData(elements, version);
this.pendingOperationBeginTimeList.shift();
this._sendingOperation = null;
this.state = STATE.SENDING;
this.sendNextOperations();
};
syncMouseLocationToOthers = throttle((payload) => {
if (payload.pointersMap.size < 2) {
const { pointer, button } = payload;
this.socketClient.broadcastMouseLocation({ pointer, button });
}
}, CURSOR_SYNC_TIMEOUT);
updateLocalDataByRemoteData = (remoteElements, remoteVersion) => {
const localElements = this.excalidrawAPI.getSceneElementsIncludingDeleted();
const appState = this.excalidrawAPI.getAppState();
const restoredRemoteElements = restoreElements(remoteElements, null);
const reconciledElements = reconcileElements(localElements, restoredRemoteElements, appState);
this.setLastBroadcastedOrReceivedSceneVersion(reconciledElements);
this.setVersion(remoteVersion);
this.excalidrawAPI.updateScene({
elements: reconciledElements,
captureUpdate: CaptureUpdateAction.NEVER,
});
// sync images from another user
this.loadImageFiles();
};
handleRemoteSceneUpdated = (params) => {
const { elements, version } = params;
this.updateLocalDataByRemoteData(elements, version);
};
handleRemoteMouseLocationUpdated = (params) => {
const collaborators = new Map(this.collaborators);
const { user, ...updates } = params;
if (!collaborators.get(user._username)) return;
const newUser = Object.assign({}, collaborators.get(user._username), updates);
collaborators.set(newUser._username, newUser);
this.collaborators = collaborators;
this.excalidrawAPI.updateScene({ collaborators });
return;
};
receiveRoomUserChanged = (users) => {
const collaborators = new Map(this.collaborators);
if (users && Array.isArray(users)) {
users.forEach(user => {
if (!collaborators.get(user._username)) {
collaborators.set(user._username, user);
}
});
this.collaborators = collaborators;
setTimeout(() => {
this.excalidrawAPI.updateScene({ collaborators });
}, 100);
}
};
receiveLeaveRoom = (userInfo) => {
const collaborators = new Map(this.collaborators);
if (collaborators.get(userInfo._username)) {
collaborators.delete(userInfo._username);
this.collaborators = collaborators;
this.excalidrawAPI.updateScene({ collaborators });
}
};
dispatchConnectState = (type, message) => {
if (type === 'reconnect') {
this.state = STATE.IDLE;
}
if (type === 'disconnect') {
// current state is sending
if (this._sendingOperation) {
this.pendingOperationList.unshift(this._sendingOperations.slice());
this._sendingOperation = null;
}
stateDebug(`State Changed: ${this.state} -> ${STATE.DISCONNECT}`);
this.state = STATE.DISCONNECT;
}
this.eventBus.dispatch(type, message);
};
static destroy = () => {
this.instance = null;
this.socketClient.close();
};
}
export default SocketManager;