feat: refactor local persistence & fix race condition on SW reload (#5032)
This commit is contained in:
parent
58fe639b8d
commit
5359e4fec9
@ -68,6 +68,7 @@ import {
|
|||||||
} from "./reconciliation";
|
} from "./reconciliation";
|
||||||
import { decryptData } from "../../data/encryption";
|
import { decryptData } from "../../data/encryption";
|
||||||
import { resetBrowserStateVersions } from "../data/tabSync";
|
import { resetBrowserStateVersions } from "../data/tabSync";
|
||||||
|
import { LocalData } from "../data/LocalData";
|
||||||
|
|
||||||
interface CollabState {
|
interface CollabState {
|
||||||
modalIsShown: boolean;
|
modalIsShown: boolean;
|
||||||
@ -109,10 +110,11 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
portal: Portal;
|
portal: Portal;
|
||||||
fileManager: FileManager;
|
fileManager: FileManager;
|
||||||
excalidrawAPI: Props["excalidrawAPI"];
|
excalidrawAPI: Props["excalidrawAPI"];
|
||||||
isCollaborating: boolean = false;
|
|
||||||
activeIntervalId: number | null;
|
activeIntervalId: number | null;
|
||||||
idleTimeoutId: number | null;
|
idleTimeoutId: number | null;
|
||||||
|
|
||||||
|
// marked as private to ensure we don't change it outside this class
|
||||||
|
private _isCollaborating: boolean = false;
|
||||||
private socketInitializationTimer?: number;
|
private socketInitializationTimer?: number;
|
||||||
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
private lastBroadcastedOrReceivedSceneVersion: number = -1;
|
||||||
private collaborators = new Map<string, Collaborator>();
|
private collaborators = new Map<string, Collaborator>();
|
||||||
@ -193,6 +195,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isCollaborating = () => this._isCollaborating;
|
||||||
|
|
||||||
private onUnload = () => {
|
private onUnload = () => {
|
||||||
this.destroySocketClient({ isUnload: true });
|
this.destroySocketClient({ isUnload: true });
|
||||||
};
|
};
|
||||||
@ -203,7 +207,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.isCollaborating &&
|
this._isCollaborating &&
|
||||||
(this.fileManager.shouldPreventUnload(syncableElements) ||
|
(this.fileManager.shouldPreventUnload(syncableElements) ||
|
||||||
!isSavedToFirebase(this.portal, syncableElements))
|
!isSavedToFirebase(this.portal, syncableElements))
|
||||||
) {
|
) {
|
||||||
@ -285,7 +289,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
this.setState({
|
this.setState({
|
||||||
activeRoomLink: "",
|
activeRoomLink: "",
|
||||||
});
|
});
|
||||||
this.isCollaborating = false;
|
this._isCollaborating = false;
|
||||||
|
LocalData.resumeSave("collaboration");
|
||||||
}
|
}
|
||||||
this.lastBroadcastedOrReceivedSceneVersion = -1;
|
this.lastBroadcastedOrReceivedSceneVersion = -1;
|
||||||
this.portal.close();
|
this.portal.close();
|
||||||
@ -353,7 +358,8 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
|
|
||||||
const scenePromise = resolvablePromise<ImportedDataState | null>();
|
const scenePromise = resolvablePromise<ImportedDataState | null>();
|
||||||
|
|
||||||
this.isCollaborating = true;
|
this._isCollaborating = true;
|
||||||
|
LocalData.pauseSave("collaboration");
|
||||||
|
|
||||||
const { default: socketIOClient } = await import(
|
const { default: socketIOClient } = await import(
|
||||||
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
/* webpackChunkName: "socketIoClient" */ "socket.io-client"
|
||||||
@ -760,7 +766,7 @@ class CollabWrapper extends PureComponent<Props, CollabState> {
|
|||||||
this.contextValue = {} as CollabAPI;
|
this.contextValue = {} as CollabAPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.contextValue.isCollaborating = () => this.isCollaborating;
|
this.contextValue.isCollaborating = this.isCollaborating;
|
||||||
this.contextValue.username = this.state.username;
|
this.contextValue.username = this.state.username;
|
||||||
this.contextValue.onPointerUpdate = this.onPointerUpdate;
|
this.contextValue.onPointerUpdate = this.onPointerUpdate;
|
||||||
this.contextValue.initializeSocketClient = this.initializeSocketClient;
|
this.contextValue.initializeSocketClient = this.initializeSocketClient;
|
||||||
|
@ -172,7 +172,7 @@ class Portal {
|
|||||||
|
|
||||||
this.queueFileUpload();
|
this.queueFileUpload();
|
||||||
|
|
||||||
if (syncAll && this.collab.isCollaborating) {
|
if (syncAll && this.collab.isCollaborating()) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
broadcastPromise,
|
broadcastPromise,
|
||||||
this.collab.saveCollabRoomToFirebase(syncableElements),
|
this.collab.saveCollabRoomToFirebase(syncableElements),
|
||||||
|
154
src/excalidraw-app/data/LocalData.ts
Normal file
154
src/excalidraw-app/data/LocalData.ts
Normal file
@ -0,0 +1,154 @@
|
|||||||
|
/**
|
||||||
|
* This file deals with saving data state (appState, elements, images, ...)
|
||||||
|
* locally to the browser.
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
*
|
||||||
|
* - DataState refers to full state of the app: appState, elements, images,
|
||||||
|
* though some state is saved separately (collab username, library) for one
|
||||||
|
* reason or another. We also save different data to different sotrage
|
||||||
|
* (localStorage, indexedDB).
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createStore, keys, del, getMany, set } from "idb-keyval";
|
||||||
|
import { clearAppStateForLocalStorage } from "../../appState";
|
||||||
|
import { clearElementsForLocalStorage } from "../../element";
|
||||||
|
import { ExcalidrawElement, FileId } from "../../element/types";
|
||||||
|
import { AppState, BinaryFileData, BinaryFiles } from "../../types";
|
||||||
|
import { debounce } from "../../utils";
|
||||||
|
import { SAVE_TO_LOCAL_STORAGE_TIMEOUT, STORAGE_KEYS } from "../app_constants";
|
||||||
|
import { FileManager } from "./FileManager";
|
||||||
|
import { Locker } from "./Locker";
|
||||||
|
import { updateBrowserStateVersion } from "./tabSync";
|
||||||
|
|
||||||
|
const filesStore = createStore("files-db", "files-store");
|
||||||
|
|
||||||
|
class LocalFileManager extends FileManager {
|
||||||
|
clearObsoleteFiles = async (opts: { currentFileIds: FileId[] }) => {
|
||||||
|
const allIds = await keys(filesStore);
|
||||||
|
for (const id of allIds) {
|
||||||
|
if (!opts.currentFileIds.includes(id as FileId)) {
|
||||||
|
del(id, filesStore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveDataStateToLocalStorage = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
||||||
|
JSON.stringify(clearElementsForLocalStorage(elements)),
|
||||||
|
);
|
||||||
|
localStorage.setItem(
|
||||||
|
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
||||||
|
JSON.stringify(clearAppStateForLocalStorage(appState)),
|
||||||
|
);
|
||||||
|
updateBrowserStateVersion(STORAGE_KEYS.VERSION_DATA_STATE);
|
||||||
|
} catch (error: any) {
|
||||||
|
// Unable to access window.localStorage
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type SavingLockTypes = "collaboration";
|
||||||
|
|
||||||
|
export class LocalData {
|
||||||
|
private static _save = debounce(
|
||||||
|
async (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
files: BinaryFiles,
|
||||||
|
onFilesSaved: () => void,
|
||||||
|
) => {
|
||||||
|
saveDataStateToLocalStorage(elements, appState);
|
||||||
|
|
||||||
|
await this.fileStorage.saveFiles({
|
||||||
|
elements,
|
||||||
|
files,
|
||||||
|
});
|
||||||
|
onFilesSaved();
|
||||||
|
},
|
||||||
|
SAVE_TO_LOCAL_STORAGE_TIMEOUT,
|
||||||
|
);
|
||||||
|
|
||||||
|
/** Saves DataState, including files. Bails if saving is paused */
|
||||||
|
static save = (
|
||||||
|
elements: readonly ExcalidrawElement[],
|
||||||
|
appState: AppState,
|
||||||
|
files: BinaryFiles,
|
||||||
|
onFilesSaved: () => void,
|
||||||
|
) => {
|
||||||
|
// we need to make the `isSavePaused` check synchronously (undebounced)
|
||||||
|
if (!this.isSavePaused()) {
|
||||||
|
this._save(elements, appState, files, onFilesSaved);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
static flushSave = () => {
|
||||||
|
this._save.flush();
|
||||||
|
};
|
||||||
|
|
||||||
|
private static locker = new Locker<SavingLockTypes>();
|
||||||
|
|
||||||
|
static pauseSave = (lockType: SavingLockTypes) => {
|
||||||
|
this.locker.lock(lockType);
|
||||||
|
};
|
||||||
|
|
||||||
|
static resumeSave = (lockType: SavingLockTypes) => {
|
||||||
|
this.locker.unlock(lockType);
|
||||||
|
};
|
||||||
|
|
||||||
|
static isSavePaused = () => {
|
||||||
|
return document.hidden || this.locker.isLocked();
|
||||||
|
};
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static fileStorage = new LocalFileManager({
|
||||||
|
getFiles(ids) {
|
||||||
|
return getMany(ids, filesStore).then(
|
||||||
|
(filesData: (BinaryFileData | undefined)[]) => {
|
||||||
|
const loadedFiles: BinaryFileData[] = [];
|
||||||
|
const erroredFiles = new Map<FileId, true>();
|
||||||
|
filesData.forEach((data, index) => {
|
||||||
|
const id = ids[index];
|
||||||
|
if (data) {
|
||||||
|
loadedFiles.push(data);
|
||||||
|
} else {
|
||||||
|
erroredFiles.set(id, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return { loadedFiles, erroredFiles };
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
async saveFiles({ addedFiles }) {
|
||||||
|
const savedFiles = new Map<FileId, true>();
|
||||||
|
const erroredFiles = new Map<FileId, true>();
|
||||||
|
|
||||||
|
// before we use `storage` event synchronization, let's update the flag
|
||||||
|
// optimistically. Hopefully nothing fails, and an IDB read executed
|
||||||
|
// before an IDB write finishes will read the latest value.
|
||||||
|
updateBrowserStateVersion(STORAGE_KEYS.VERSION_FILES);
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
[...addedFiles].map(async ([id, fileData]) => {
|
||||||
|
try {
|
||||||
|
await set(id, fileData, filesStore);
|
||||||
|
savedFiles.set(id, true);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
erroredFiles.set(id, true);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
return { savedFiles, erroredFiles };
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
18
src/excalidraw-app/data/Locker.ts
Normal file
18
src/excalidraw-app/data/Locker.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export class Locker<T extends string> {
|
||||||
|
private locks = new Map<T, true>();
|
||||||
|
|
||||||
|
lock = (lockType: T) => {
|
||||||
|
this.locks.set(lockType, true);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @returns whether no locks remaining */
|
||||||
|
unlock = (lockType: T) => {
|
||||||
|
this.locks.delete(lockType);
|
||||||
|
return !this.isLocked();
|
||||||
|
};
|
||||||
|
|
||||||
|
/** @returns whether some (or specific) locks are present */
|
||||||
|
isLocked(lockType?: T) {
|
||||||
|
return lockType ? this.locks.has(lockType) : !!this.locks.size;
|
||||||
|
}
|
||||||
|
}
|
@ -5,7 +5,6 @@ import {
|
|||||||
getDefaultAppState,
|
getDefaultAppState,
|
||||||
} from "../../appState";
|
} from "../../appState";
|
||||||
import { clearElementsForLocalStorage } from "../../element";
|
import { clearElementsForLocalStorage } from "../../element";
|
||||||
import { updateBrowserStateVersion } from "./tabSync";
|
|
||||||
import { STORAGE_KEYS } from "../app_constants";
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
|
|
||||||
export const saveUsernameToLocalStorage = (username: string) => {
|
export const saveUsernameToLocalStorage = (username: string) => {
|
||||||
@ -34,26 +33,6 @@ export const importUsernameFromLocalStorage = (): string | null => {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const saveToLocalStorage = (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
appState: AppState,
|
|
||||||
) => {
|
|
||||||
try {
|
|
||||||
localStorage.setItem(
|
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
|
||||||
JSON.stringify(clearElementsForLocalStorage(elements)),
|
|
||||||
);
|
|
||||||
localStorage.setItem(
|
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
|
||||||
JSON.stringify(clearAppStateForLocalStorage(appState)),
|
|
||||||
);
|
|
||||||
updateBrowserStateVersion(STORAGE_KEYS.VERSION_DATA_STATE);
|
|
||||||
} catch (error: any) {
|
|
||||||
// Unable to access window.localStorage
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const importFromLocalStorage = () => {
|
export const importFromLocalStorage = () => {
|
||||||
let savedElements = null;
|
let savedElements = null;
|
||||||
let savedState = null;
|
let savedState = null;
|
||||||
|
@ -28,7 +28,6 @@ import {
|
|||||||
AppState,
|
AppState,
|
||||||
LibraryItems,
|
LibraryItems,
|
||||||
ExcalidrawImperativeAPI,
|
ExcalidrawImperativeAPI,
|
||||||
BinaryFileData,
|
|
||||||
BinaryFiles,
|
BinaryFiles,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import {
|
import {
|
||||||
@ -42,7 +41,6 @@ import {
|
|||||||
} from "../utils";
|
} from "../utils";
|
||||||
import {
|
import {
|
||||||
FIREBASE_STORAGE_PREFIXES,
|
FIREBASE_STORAGE_PREFIXES,
|
||||||
SAVE_TO_LOCAL_STORAGE_TIMEOUT,
|
|
||||||
STORAGE_KEYS,
|
STORAGE_KEYS,
|
||||||
SYNC_BROWSER_TABS_TIMEOUT,
|
SYNC_BROWSER_TABS_TIMEOUT,
|
||||||
} from "./app_constants";
|
} from "./app_constants";
|
||||||
@ -57,7 +55,6 @@ import {
|
|||||||
getLibraryItemsFromStorage,
|
getLibraryItemsFromStorage,
|
||||||
importFromLocalStorage,
|
importFromLocalStorage,
|
||||||
importUsernameFromLocalStorage,
|
importUsernameFromLocalStorage,
|
||||||
saveToLocalStorage,
|
|
||||||
} from "./data/localStorage";
|
} from "./data/localStorage";
|
||||||
import CustomStats from "./CustomStats";
|
import CustomStats from "./CustomStats";
|
||||||
import { restoreAppState, RestoredDataState } from "../data/restore";
|
import { restoreAppState, RestoredDataState } from "../data/restore";
|
||||||
@ -67,72 +64,12 @@ import { shield } from "../components/icons";
|
|||||||
import "./index.scss";
|
import "./index.scss";
|
||||||
import { ExportToExcalidrawPlus } from "./components/ExportToExcalidrawPlus";
|
import { ExportToExcalidrawPlus } from "./components/ExportToExcalidrawPlus";
|
||||||
|
|
||||||
import { getMany, set, del, keys, createStore } from "idb-keyval";
|
import { updateStaleImageStatuses } from "./data/FileManager";
|
||||||
import { FileManager, updateStaleImageStatuses } from "./data/FileManager";
|
|
||||||
import { newElementWith } from "../element/mutateElement";
|
import { newElementWith } from "../element/mutateElement";
|
||||||
import { isInitializedImageElement } from "../element/typeChecks";
|
import { isInitializedImageElement } from "../element/typeChecks";
|
||||||
import { loadFilesFromFirebase } from "./data/firebase";
|
import { loadFilesFromFirebase } from "./data/firebase";
|
||||||
import {
|
import { LocalData } from "./data/LocalData";
|
||||||
isBrowserStorageStateNewer,
|
import { isBrowserStorageStateNewer } from "./data/tabSync";
|
||||||
updateBrowserStateVersion,
|
|
||||||
} from "./data/tabSync";
|
|
||||||
|
|
||||||
const filesStore = createStore("files-db", "files-store");
|
|
||||||
|
|
||||||
const clearObsoleteFilesFromIndexedDB = async (opts: {
|
|
||||||
currentFileIds: FileId[];
|
|
||||||
}) => {
|
|
||||||
const allIds = await keys(filesStore);
|
|
||||||
for (const id of allIds) {
|
|
||||||
if (!opts.currentFileIds.includes(id as FileId)) {
|
|
||||||
del(id, filesStore);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const localFileStorage = new FileManager({
|
|
||||||
getFiles(ids) {
|
|
||||||
return getMany(ids, filesStore).then(
|
|
||||||
(filesData: (BinaryFileData | undefined)[]) => {
|
|
||||||
const loadedFiles: BinaryFileData[] = [];
|
|
||||||
const erroredFiles = new Map<FileId, true>();
|
|
||||||
filesData.forEach((data, index) => {
|
|
||||||
const id = ids[index];
|
|
||||||
if (data) {
|
|
||||||
loadedFiles.push(data);
|
|
||||||
} else {
|
|
||||||
erroredFiles.set(id, true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return { loadedFiles, erroredFiles };
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
async saveFiles({ addedFiles }) {
|
|
||||||
const savedFiles = new Map<FileId, true>();
|
|
||||||
const erroredFiles = new Map<FileId, true>();
|
|
||||||
|
|
||||||
// before we use `storage` event synchronization, let's update the flag
|
|
||||||
// optimistically. Hopefully nothing fails, and an IDB read executed
|
|
||||||
// before an IDB write finishes will read the latest value.
|
|
||||||
updateBrowserStateVersion(STORAGE_KEYS.VERSION_FILES);
|
|
||||||
|
|
||||||
await Promise.all(
|
|
||||||
[...addedFiles].map(async ([id, fileData]) => {
|
|
||||||
try {
|
|
||||||
await set(id, fileData, filesStore);
|
|
||||||
savedFiles.set(id, true);
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error(error);
|
|
||||||
erroredFiles.set(id, true);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return { savedFiles, erroredFiles };
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const languageDetector = new LanguageDetector();
|
const languageDetector = new LanguageDetector();
|
||||||
languageDetector.init({
|
languageDetector.init({
|
||||||
@ -143,28 +80,6 @@ languageDetector.init({
|
|||||||
checkWhitelist: false,
|
checkWhitelist: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
const saveDebounced = debounce(
|
|
||||||
async (
|
|
||||||
elements: readonly ExcalidrawElement[],
|
|
||||||
appState: AppState,
|
|
||||||
files: BinaryFiles,
|
|
||||||
onFilesSaved: () => void,
|
|
||||||
) => {
|
|
||||||
saveToLocalStorage(elements, appState);
|
|
||||||
|
|
||||||
await localFileStorage.saveFiles({
|
|
||||||
elements,
|
|
||||||
files,
|
|
||||||
});
|
|
||||||
onFilesSaved();
|
|
||||||
},
|
|
||||||
SAVE_TO_LOCAL_STORAGE_TIMEOUT,
|
|
||||||
);
|
|
||||||
|
|
||||||
const onBlur = () => {
|
|
||||||
saveDebounced.flush();
|
|
||||||
};
|
|
||||||
|
|
||||||
const initializeScene = async (opts: {
|
const initializeScene = async (opts: {
|
||||||
collabAPI: CollabAPI;
|
collabAPI: CollabAPI;
|
||||||
}): Promise<
|
}): Promise<
|
||||||
@ -366,7 +281,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
});
|
});
|
||||||
} else if (isInitialLoad) {
|
} else if (isInitialLoad) {
|
||||||
if (fileIds.length) {
|
if (fileIds.length) {
|
||||||
localFileStorage
|
LocalData.fileStorage
|
||||||
.getFiles(fileIds)
|
.getFiles(fileIds)
|
||||||
.then(({ loadedFiles, erroredFiles }) => {
|
.then(({ loadedFiles, erroredFiles }) => {
|
||||||
if (loadedFiles.length) {
|
if (loadedFiles.length) {
|
||||||
@ -381,7 +296,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
// on fresh load, clear unused files from IDB (from previous
|
// on fresh load, clear unused files from IDB (from previous
|
||||||
// session)
|
// session)
|
||||||
clearObsoleteFilesFromIndexedDB({ currentFileIds: fileIds });
|
LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -458,7 +373,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
return acc;
|
return acc;
|
||||||
}, [] as FileId[]) || [];
|
}, [] as FileId[]) || [];
|
||||||
if (fileIds.length) {
|
if (fileIds.length) {
|
||||||
localFileStorage
|
LocalData.fileStorage
|
||||||
.getFiles(fileIds)
|
.getFiles(fileIds)
|
||||||
.then(({ loadedFiles, erroredFiles }) => {
|
.then(({ loadedFiles, erroredFiles }) => {
|
||||||
if (loadedFiles.length) {
|
if (loadedFiles.length) {
|
||||||
@ -475,28 +390,50 @@ const ExcalidrawWrapper = () => {
|
|||||||
}
|
}
|
||||||
}, SYNC_BROWSER_TABS_TIMEOUT);
|
}, SYNC_BROWSER_TABS_TIMEOUT);
|
||||||
|
|
||||||
|
const onUnload = () => {
|
||||||
|
LocalData.flushSave();
|
||||||
|
};
|
||||||
|
|
||||||
|
const visibilityChange = (event: FocusEvent | Event) => {
|
||||||
|
if (event.type === EVENT.BLUR || document.hidden) {
|
||||||
|
LocalData.flushSave();
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
event.type === EVENT.VISIBILITY_CHANGE ||
|
||||||
|
event.type === EVENT.FOCUS
|
||||||
|
) {
|
||||||
|
syncData();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
window.addEventListener(EVENT.HASHCHANGE, onHashChange, false);
|
window.addEventListener(EVENT.HASHCHANGE, onHashChange, false);
|
||||||
window.addEventListener(EVENT.UNLOAD, onBlur, false);
|
window.addEventListener(EVENT.UNLOAD, onUnload, false);
|
||||||
window.addEventListener(EVENT.BLUR, onBlur, false);
|
window.addEventListener(EVENT.BLUR, visibilityChange, false);
|
||||||
document.addEventListener(EVENT.VISIBILITY_CHANGE, syncData, false);
|
document.addEventListener(EVENT.VISIBILITY_CHANGE, visibilityChange, false);
|
||||||
window.addEventListener(EVENT.FOCUS, syncData, false);
|
window.addEventListener(EVENT.FOCUS, visibilityChange, false);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
|
window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
|
||||||
window.removeEventListener(EVENT.UNLOAD, onBlur, false);
|
window.removeEventListener(EVENT.UNLOAD, onUnload, false);
|
||||||
window.removeEventListener(EVENT.BLUR, onBlur, false);
|
window.removeEventListener(EVENT.BLUR, visibilityChange, false);
|
||||||
window.removeEventListener(EVENT.FOCUS, syncData, false);
|
window.removeEventListener(EVENT.FOCUS, visibilityChange, false);
|
||||||
document.removeEventListener(EVENT.VISIBILITY_CHANGE, syncData, false);
|
document.removeEventListener(
|
||||||
|
EVENT.VISIBILITY_CHANGE,
|
||||||
|
visibilityChange,
|
||||||
|
false,
|
||||||
|
);
|
||||||
clearTimeout(titleTimeout);
|
clearTimeout(titleTimeout);
|
||||||
};
|
};
|
||||||
}, [collabAPI, excalidrawAPI]);
|
}, [collabAPI, excalidrawAPI]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const unloadHandler = (event: BeforeUnloadEvent) => {
|
const unloadHandler = (event: BeforeUnloadEvent) => {
|
||||||
saveDebounced.flush();
|
LocalData.flushSave();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
excalidrawAPI &&
|
excalidrawAPI &&
|
||||||
localFileStorage.shouldPreventUnload(excalidrawAPI.getSceneElements())
|
LocalData.fileStorage.shouldPreventUnload(
|
||||||
|
excalidrawAPI.getSceneElements(),
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
preventUnload(event);
|
preventUnload(event);
|
||||||
}
|
}
|
||||||
@ -518,8 +455,12 @@ const ExcalidrawWrapper = () => {
|
|||||||
) => {
|
) => {
|
||||||
if (collabAPI?.isCollaborating()) {
|
if (collabAPI?.isCollaborating()) {
|
||||||
collabAPI.broadcastElements(elements);
|
collabAPI.broadcastElements(elements);
|
||||||
} else {
|
}
|
||||||
saveDebounced(elements, appState, files, () => {
|
|
||||||
|
// this check is redundant, but since this is a hot path, it's best
|
||||||
|
// not to evaludate the nested expression every time
|
||||||
|
if (!LocalData.isSavePaused()) {
|
||||||
|
LocalData.save(elements, appState, files, () => {
|
||||||
if (excalidrawAPI) {
|
if (excalidrawAPI) {
|
||||||
let didChange = false;
|
let didChange = false;
|
||||||
|
|
||||||
@ -527,7 +468,9 @@ const ExcalidrawWrapper = () => {
|
|||||||
const elements = excalidrawAPI
|
const elements = excalidrawAPI
|
||||||
.getSceneElementsIncludingDeleted()
|
.getSceneElementsIncludingDeleted()
|
||||||
.map((element) => {
|
.map((element) => {
|
||||||
if (localFileStorage.shouldUpdateImageElementStatus(element)) {
|
if (
|
||||||
|
LocalData.fileStorage.shouldUpdateImageElementStatus(element)
|
||||||
|
) {
|
||||||
didChange = true;
|
didChange = true;
|
||||||
const newEl = newElementWith(element, { status: "saved" });
|
const newEl = newElementWith(element, { status: "saved" });
|
||||||
if (pendingImageElement === element) {
|
if (pendingImageElement === element) {
|
||||||
@ -687,7 +630,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onRoomClose = useCallback(() => {
|
const onRoomClose = useCallback(() => {
|
||||||
localFileStorage.reset();
|
LocalData.fileStorage.reset();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
Loading…
x
Reference in New Issue
Block a user