feat: refactor local persistence & fix race condition on SW reload (#5032)

This commit is contained in:
David Luzar 2022-04-11 22:15:49 +02:00 committed by GitHub
parent 58fe639b8d
commit 5359e4fec9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 232 additions and 132 deletions

View file

@ -28,7 +28,6 @@ import {
AppState,
LibraryItems,
ExcalidrawImperativeAPI,
BinaryFileData,
BinaryFiles,
} from "../types";
import {
@ -42,7 +41,6 @@ import {
} from "../utils";
import {
FIREBASE_STORAGE_PREFIXES,
SAVE_TO_LOCAL_STORAGE_TIMEOUT,
STORAGE_KEYS,
SYNC_BROWSER_TABS_TIMEOUT,
} from "./app_constants";
@ -57,7 +55,6 @@ import {
getLibraryItemsFromStorage,
importFromLocalStorage,
importUsernameFromLocalStorage,
saveToLocalStorage,
} from "./data/localStorage";
import CustomStats from "./CustomStats";
import { restoreAppState, RestoredDataState } from "../data/restore";
@ -67,72 +64,12 @@ import { shield } from "../components/icons";
import "./index.scss";
import { ExportToExcalidrawPlus } from "./components/ExportToExcalidrawPlus";
import { getMany, set, del, keys, createStore } from "idb-keyval";
import { FileManager, updateStaleImageStatuses } from "./data/FileManager";
import { updateStaleImageStatuses } from "./data/FileManager";
import { newElementWith } from "../element/mutateElement";
import { isInitializedImageElement } from "../element/typeChecks";
import { loadFilesFromFirebase } from "./data/firebase";
import {
isBrowserStorageStateNewer,
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 };
},
});
import { LocalData } from "./data/LocalData";
import { isBrowserStorageStateNewer } from "./data/tabSync";
const languageDetector = new LanguageDetector();
languageDetector.init({
@ -143,28 +80,6 @@ languageDetector.init({
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: {
collabAPI: CollabAPI;
}): Promise<
@ -366,7 +281,7 @@ const ExcalidrawWrapper = () => {
});
} else if (isInitialLoad) {
if (fileIds.length) {
localFileStorage
LocalData.fileStorage
.getFiles(fileIds)
.then(({ loadedFiles, erroredFiles }) => {
if (loadedFiles.length) {
@ -381,7 +296,7 @@ const ExcalidrawWrapper = () => {
}
// on fresh load, clear unused files from IDB (from previous
// session)
clearObsoleteFilesFromIndexedDB({ currentFileIds: fileIds });
LocalData.fileStorage.clearObsoleteFiles({ currentFileIds: fileIds });
}
}
@ -458,7 +373,7 @@ const ExcalidrawWrapper = () => {
return acc;
}, [] as FileId[]) || [];
if (fileIds.length) {
localFileStorage
LocalData.fileStorage
.getFiles(fileIds)
.then(({ loadedFiles, erroredFiles }) => {
if (loadedFiles.length) {
@ -475,28 +390,50 @@ const ExcalidrawWrapper = () => {
}
}, 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.UNLOAD, onBlur, false);
window.addEventListener(EVENT.BLUR, onBlur, false);
document.addEventListener(EVENT.VISIBILITY_CHANGE, syncData, false);
window.addEventListener(EVENT.FOCUS, syncData, false);
window.addEventListener(EVENT.UNLOAD, onUnload, false);
window.addEventListener(EVENT.BLUR, visibilityChange, false);
document.addEventListener(EVENT.VISIBILITY_CHANGE, visibilityChange, false);
window.addEventListener(EVENT.FOCUS, visibilityChange, false);
return () => {
window.removeEventListener(EVENT.HASHCHANGE, onHashChange, false);
window.removeEventListener(EVENT.UNLOAD, onBlur, false);
window.removeEventListener(EVENT.BLUR, onBlur, false);
window.removeEventListener(EVENT.FOCUS, syncData, false);
document.removeEventListener(EVENT.VISIBILITY_CHANGE, syncData, false);
window.removeEventListener(EVENT.UNLOAD, onUnload, false);
window.removeEventListener(EVENT.BLUR, visibilityChange, false);
window.removeEventListener(EVENT.FOCUS, visibilityChange, false);
document.removeEventListener(
EVENT.VISIBILITY_CHANGE,
visibilityChange,
false,
);
clearTimeout(titleTimeout);
};
}, [collabAPI, excalidrawAPI]);
useEffect(() => {
const unloadHandler = (event: BeforeUnloadEvent) => {
saveDebounced.flush();
LocalData.flushSave();
if (
excalidrawAPI &&
localFileStorage.shouldPreventUnload(excalidrawAPI.getSceneElements())
LocalData.fileStorage.shouldPreventUnload(
excalidrawAPI.getSceneElements(),
)
) {
preventUnload(event);
}
@ -518,8 +455,12 @@ const ExcalidrawWrapper = () => {
) => {
if (collabAPI?.isCollaborating()) {
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) {
let didChange = false;
@ -527,7 +468,9 @@ const ExcalidrawWrapper = () => {
const elements = excalidrawAPI
.getSceneElementsIncludingDeleted()
.map((element) => {
if (localFileStorage.shouldUpdateImageElementStatus(element)) {
if (
LocalData.fileStorage.shouldUpdateImageElementStatus(element)
) {
didChange = true;
const newEl = newElementWith(element, { status: "saved" });
if (pendingImageElement === element) {
@ -687,7 +630,7 @@ const ExcalidrawWrapper = () => {
};
const onRoomClose = useCallback(() => {
localFileStorage.reset();
LocalData.fileStorage.reset();
}, []);
return (