123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258 |
- /**
- * 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 storage
- * (localStorage, indexedDB).
- */
- import { clearAppStateForLocalStorage } from "@excalidraw/excalidraw/appState";
- import {
- CANVAS_SEARCH_TAB,
- DEFAULT_SIDEBAR,
- debounce,
- } from "@excalidraw/common";
- import { clearElementsForLocalStorage } from "@excalidraw/element";
- import {
- createStore,
- entries,
- del,
- getMany,
- set,
- setMany,
- get,
- } from "idb-keyval";
- import type { LibraryPersistedData } from "@excalidraw/excalidraw/data/library";
- import type { ImportedDataState } from "@excalidraw/excalidraw/data/types";
- import type { ExcalidrawElement, FileId } from "@excalidraw/element/types";
- import type {
- AppState,
- BinaryFileData,
- BinaryFiles,
- } from "@excalidraw/excalidraw/types";
- import type { MaybePromise } from "@excalidraw/common/utility-types";
- 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[] }) => {
- await entries(filesStore).then((entries) => {
- for (const [id, imageData] of entries as [FileId, BinaryFileData][]) {
- // if image is unused (not on canvas) & is older than 1 day, delete it
- // from storage. We check `lastRetrieved` we care about the last time
- // the image was used (loaded on canvas), not when it was initially
- // created.
- if (
- (!imageData.lastRetrieved ||
- Date.now() - imageData.lastRetrieved > 24 * 3600 * 1000) &&
- !opts.currentFileIds.includes(id as FileId)
- ) {
- del(id, filesStore);
- }
- }
- });
- };
- }
- const saveDataStateToLocalStorage = (
- elements: readonly ExcalidrawElement[],
- appState: AppState,
- ) => {
- try {
- const _appState = clearAppStateForLocalStorage(appState);
- if (
- _appState.openSidebar?.name === DEFAULT_SIDEBAR.name &&
- _appState.openSidebar.tab === CANVAS_SEARCH_TAB
- ) {
- _appState.openSidebar = null;
- }
- localStorage.setItem(
- STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
- JSON.stringify(clearElementsForLocalStorage(elements)),
- );
- localStorage.setItem(
- STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
- JSON.stringify(_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(
- async (filesData: (BinaryFileData | undefined)[]) => {
- const loadedFiles: BinaryFileData[] = [];
- const erroredFiles = new Map<FileId, true>();
- const filesToSave: [FileId, BinaryFileData][] = [];
- filesData.forEach((data, index) => {
- const id = ids[index];
- if (data) {
- const _data: BinaryFileData = {
- ...data,
- lastRetrieved: Date.now(),
- };
- filesToSave.push([id, _data]);
- loadedFiles.push(_data);
- } else {
- erroredFiles.set(id, true);
- }
- });
- try {
- // save loaded files back to storage with updated `lastRetrieved`
- setMany(filesToSave, filesStore);
- } catch (error) {
- console.warn(error);
- }
- return { loadedFiles, erroredFiles };
- },
- );
- },
- async saveFiles({ addedFiles }) {
- const savedFiles = new Map<FileId, BinaryFileData>();
- const erroredFiles = new Map<FileId, BinaryFileData>();
- // 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, fileData);
- } catch (error: any) {
- console.error(error);
- erroredFiles.set(id, fileData);
- }
- }),
- );
- return { savedFiles, erroredFiles };
- },
- });
- }
- export class LibraryIndexedDBAdapter {
- /** IndexedDB database and store name */
- private static idb_name = STORAGE_KEYS.IDB_LIBRARY;
- /** library data store key */
- private static key = "libraryData";
- private static store = createStore(
- `${LibraryIndexedDBAdapter.idb_name}-db`,
- `${LibraryIndexedDBAdapter.idb_name}-store`,
- );
- static async load() {
- const IDBData = await get<LibraryPersistedData>(
- LibraryIndexedDBAdapter.key,
- LibraryIndexedDBAdapter.store,
- );
- return IDBData || null;
- }
- static save(data: LibraryPersistedData): MaybePromise<void> {
- return set(
- LibraryIndexedDBAdapter.key,
- data,
- LibraryIndexedDBAdapter.store,
- );
- }
- }
- /** LS Adapter used only for migrating LS library data
- * to indexedDB */
- export class LibraryLocalStorageMigrationAdapter {
- static load() {
- const LSData = localStorage.getItem(
- STORAGE_KEYS.__LEGACY_LOCAL_STORAGE_LIBRARY,
- );
- if (LSData != null) {
- const libraryItems: ImportedDataState["libraryItems"] =
- JSON.parse(LSData);
- if (libraryItems) {
- return { libraryItems };
- }
- }
- return null;
- }
- static clear() {
- localStorage.removeItem(STORAGE_KEYS.__LEGACY_LOCAL_STORAGE_LIBRARY);
- }
- }
|