mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-08-03 18:27:04 -04:00
fix: use idb for elements and app state
This commit is contained in:
parent
37ad85cbaf
commit
cff7516318
@ -112,7 +112,9 @@ import {
|
|||||||
import { updateStaleImageStatuses } from "./data/FileManager";
|
import { updateStaleImageStatuses } from "./data/FileManager";
|
||||||
import {
|
import {
|
||||||
importFromLocalStorage,
|
importFromLocalStorage,
|
||||||
|
importFromIndexedDB,
|
||||||
importUsernameFromLocalStorage,
|
importUsernameFromLocalStorage,
|
||||||
|
migrateFromLocalStorageToIndexedDB,
|
||||||
} from "./data/localStorage";
|
} from "./data/localStorage";
|
||||||
|
|
||||||
import { loadFilesFromFirebase } from "./data/firebase";
|
import { loadFilesFromFirebase } from "./data/firebase";
|
||||||
@ -218,7 +220,15 @@ const initializeScene = async (opts: {
|
|||||||
);
|
);
|
||||||
const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
|
const externalUrlMatch = window.location.hash.match(/^#url=(.*)$/);
|
||||||
|
|
||||||
const localDataState = importFromLocalStorage();
|
// migrate from localStorage to IndexedDB if needed
|
||||||
|
await migrateFromLocalStorageToIndexedDB();
|
||||||
|
|
||||||
|
// try to load from IndexedDB first, fallback to localStorage
|
||||||
|
let localDataState = await importFromIndexedDB();
|
||||||
|
if (!localDataState.elements.length && !localDataState.appState) {
|
||||||
|
// fallback to localStorage if IndexedDB is empty
|
||||||
|
localDataState = importFromLocalStorage();
|
||||||
|
}
|
||||||
|
|
||||||
let scene: RestoredDataState & {
|
let scene: RestoredDataState & {
|
||||||
scrollToContent?: boolean;
|
scrollToContent?: boolean;
|
||||||
@ -504,7 +514,7 @@ const ExcalidrawWrapper = () => {
|
|||||||
TITLE_TIMEOUT,
|
TITLE_TIMEOUT,
|
||||||
);
|
);
|
||||||
|
|
||||||
const syncData = debounce(() => {
|
const syncData = debounce(async () => {
|
||||||
if (isTestEnv()) {
|
if (isTestEnv()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -514,7 +524,12 @@ const ExcalidrawWrapper = () => {
|
|||||||
) {
|
) {
|
||||||
// don't sync if local state is newer or identical to browser state
|
// don't sync if local state is newer or identical to browser state
|
||||||
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
|
if (isBrowserStorageStateNewer(STORAGE_KEYS.VERSION_DATA_STATE)) {
|
||||||
const localDataState = importFromLocalStorage();
|
// try to load from IndexedDB first, fallback to localStorage
|
||||||
|
let localDataState = await importFromIndexedDB();
|
||||||
|
if (!localDataState.elements.length && !localDataState.appState) {
|
||||||
|
// fallback to localStorage if IndexedDB is empty
|
||||||
|
localDataState = importFromLocalStorage();
|
||||||
|
}
|
||||||
const username = importUsernameFromLocalStorage();
|
const username = importUsernameFromLocalStorage();
|
||||||
setLangCode(getPreferredLanguage());
|
setLangCode(getPreferredLanguage());
|
||||||
excalidrawAPI.updateScene({
|
excalidrawAPI.updateScene({
|
||||||
|
@ -21,11 +21,23 @@ type StorageSizes = { scene: number; total: number };
|
|||||||
|
|
||||||
const STORAGE_SIZE_TIMEOUT = 500;
|
const STORAGE_SIZE_TIMEOUT = 500;
|
||||||
|
|
||||||
const getStorageSizes = debounce((cb: (sizes: StorageSizes) => void) => {
|
const getStorageSizes = debounce(async (cb: (sizes: StorageSizes) => void) => {
|
||||||
cb({
|
try {
|
||||||
scene: getElementsStorageSize(),
|
const [scene, total] = await Promise.all([
|
||||||
total: getTotalStorageSize(),
|
getElementsStorageSize(),
|
||||||
});
|
getTotalStorageSize(),
|
||||||
|
]);
|
||||||
|
cb({
|
||||||
|
scene,
|
||||||
|
total,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get storage sizes:", error);
|
||||||
|
cb({
|
||||||
|
scene: 0,
|
||||||
|
total: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
}, STORAGE_SIZE_TIMEOUT);
|
}, STORAGE_SIZE_TIMEOUT);
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
|
@ -65,7 +65,7 @@ class LocalFileManager extends FileManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const saveDataStateToLocalStorage = (
|
const saveDataStateToIndexedDB = async (
|
||||||
elements: readonly ExcalidrawElement[],
|
elements: readonly ExcalidrawElement[],
|
||||||
appState: AppState,
|
appState: AppState,
|
||||||
) => {
|
) => {
|
||||||
@ -79,17 +79,15 @@ const saveDataStateToLocalStorage = (
|
|||||||
_appState.openSidebar = null;
|
_appState.openSidebar = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
localStorage.setItem(
|
// save to IndexedDB
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
await Promise.all([
|
||||||
JSON.stringify(clearElementsForLocalStorage(elements)),
|
ElementsIndexedDBAdapter.save(clearElementsForLocalStorage(elements)),
|
||||||
);
|
AppStateIndexedDBAdapter.save(_appState),
|
||||||
localStorage.setItem(
|
]);
|
||||||
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
|
||||||
JSON.stringify(_appState),
|
|
||||||
);
|
|
||||||
updateBrowserStateVersion(STORAGE_KEYS.VERSION_DATA_STATE);
|
updateBrowserStateVersion(STORAGE_KEYS.VERSION_DATA_STATE);
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// Unable to access window.localStorage
|
// unable to access IndexedDB
|
||||||
console.error(error);
|
console.error(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -104,7 +102,7 @@ export class LocalData {
|
|||||||
files: BinaryFiles,
|
files: BinaryFiles,
|
||||||
onFilesSaved: () => void,
|
onFilesSaved: () => void,
|
||||||
) => {
|
) => {
|
||||||
saveDataStateToLocalStorage(elements, appState);
|
await saveDataStateToIndexedDB(elements, appState);
|
||||||
|
|
||||||
await this.fileStorage.saveFiles({
|
await this.fileStorage.saveFiles({
|
||||||
elements,
|
elements,
|
||||||
@ -256,3 +254,63 @@ export class LibraryLocalStorageMigrationAdapter {
|
|||||||
localStorage.removeItem(STORAGE_KEYS.__LEGACY_LOCAL_STORAGE_LIBRARY);
|
localStorage.removeItem(STORAGE_KEYS.__LEGACY_LOCAL_STORAGE_LIBRARY);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** IndexedDB Adapter for storing app state */
|
||||||
|
export class AppStateIndexedDBAdapter {
|
||||||
|
/** IndexedDB database and store name */
|
||||||
|
private static idb_name = "excalidraw-app-state";
|
||||||
|
/** app state data store key */
|
||||||
|
private static key = "appStateData";
|
||||||
|
|
||||||
|
private static store = createStore(
|
||||||
|
`${AppStateIndexedDBAdapter.idb_name}-db`,
|
||||||
|
`${AppStateIndexedDBAdapter.idb_name}-store`,
|
||||||
|
);
|
||||||
|
|
||||||
|
static async load() {
|
||||||
|
const IDBData = await get<Partial<AppState>>(
|
||||||
|
AppStateIndexedDBAdapter.key,
|
||||||
|
AppStateIndexedDBAdapter.store,
|
||||||
|
);
|
||||||
|
|
||||||
|
return IDBData || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static save(data: Partial<AppState>): MaybePromise<void> {
|
||||||
|
return set(
|
||||||
|
AppStateIndexedDBAdapter.key,
|
||||||
|
data,
|
||||||
|
AppStateIndexedDBAdapter.store,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** IndexedDB Adapter for storing elements */
|
||||||
|
export class ElementsIndexedDBAdapter {
|
||||||
|
/** IndexedDB database and store name */
|
||||||
|
private static idb_name = "excalidraw-elements";
|
||||||
|
/** elements data store key */
|
||||||
|
private static key = "elementsData";
|
||||||
|
|
||||||
|
private static store = createStore(
|
||||||
|
`${ElementsIndexedDBAdapter.idb_name}-db`,
|
||||||
|
`${ElementsIndexedDBAdapter.idb_name}-store`,
|
||||||
|
);
|
||||||
|
|
||||||
|
static async load() {
|
||||||
|
const IDBData = await get<ExcalidrawElement[]>(
|
||||||
|
ElementsIndexedDBAdapter.key,
|
||||||
|
ElementsIndexedDBAdapter.store,
|
||||||
|
);
|
||||||
|
|
||||||
|
return IDBData || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static save(data: ExcalidrawElement[]): MaybePromise<void> {
|
||||||
|
return set(
|
||||||
|
ElementsIndexedDBAdapter.key,
|
||||||
|
data,
|
||||||
|
ElementsIndexedDBAdapter.store,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -9,6 +9,11 @@ import type { AppState } from "@excalidraw/excalidraw/types";
|
|||||||
|
|
||||||
import { STORAGE_KEYS } from "../app_constants";
|
import { STORAGE_KEYS } from "../app_constants";
|
||||||
|
|
||||||
|
import {
|
||||||
|
AppStateIndexedDBAdapter,
|
||||||
|
ElementsIndexedDBAdapter,
|
||||||
|
} from "./LocalData";
|
||||||
|
|
||||||
export const saveUsernameToLocalStorage = (username: string) => {
|
export const saveUsernameToLocalStorage = (username: string) => {
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
@ -74,28 +79,146 @@ export const importFromLocalStorage = () => {
|
|||||||
return { elements, appState };
|
return { elements, appState };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getElementsStorageSize = () => {
|
export const importFromIndexedDB = async () => {
|
||||||
|
let savedElements = null;
|
||||||
|
let savedState = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const elements = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS);
|
savedElements = await ElementsIndexedDBAdapter.load();
|
||||||
const elementsSize = elements?.length || 0;
|
savedState = await AppStateIndexedDBAdapter.load();
|
||||||
return elementsSize;
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
|
// unable to access IndexedDB
|
||||||
console.error(error);
|
console.error(error);
|
||||||
return 0;
|
}
|
||||||
|
|
||||||
|
let elements: ExcalidrawElement[] = [];
|
||||||
|
if (savedElements) {
|
||||||
|
try {
|
||||||
|
elements = clearElementsForLocalStorage(savedElements);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let appState = null;
|
||||||
|
if (savedState) {
|
||||||
|
try {
|
||||||
|
appState = {
|
||||||
|
...getDefaultAppState(),
|
||||||
|
...clearAppStateForLocalStorage(savedState),
|
||||||
|
};
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { elements, appState };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const migrateFromLocalStorageToIndexedDB = async () => {
|
||||||
|
try {
|
||||||
|
// check if we have data in localStorage
|
||||||
|
const savedElements = localStorage.getItem(
|
||||||
|
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
||||||
|
);
|
||||||
|
const savedState = localStorage.getItem(
|
||||||
|
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (savedElements || savedState) {
|
||||||
|
// parse and migrate elements
|
||||||
|
if (savedElements) {
|
||||||
|
try {
|
||||||
|
const elements = JSON.parse(savedElements);
|
||||||
|
await ElementsIndexedDBAdapter.save(elements);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to migrate elements:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse and migrate app state
|
||||||
|
if (savedState) {
|
||||||
|
try {
|
||||||
|
const appState = JSON.parse(savedState);
|
||||||
|
await AppStateIndexedDBAdapter.save(appState);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to migrate app state:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// clear localStorage after successful migration
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS);
|
||||||
|
localStorage.removeItem(STORAGE_KEYS.LOCAL_STORAGE_APP_STATE);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Migration failed:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getTotalStorageSize = () => {
|
/**
|
||||||
|
* Get the size of elements stored in IndexedDB (with localStorage fallback)
|
||||||
|
* @returns Promise<number> - Size in bytes
|
||||||
|
*/
|
||||||
|
export const getElementsStorageSize = async () => {
|
||||||
try {
|
try {
|
||||||
const appState = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_APP_STATE);
|
const elements = await ElementsIndexedDBAdapter.load();
|
||||||
|
if (elements) {
|
||||||
|
// calculate size by stringifying the data
|
||||||
|
const elementsString = JSON.stringify(elements);
|
||||||
|
return elementsString.length;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error("Failed to get elements size from IndexedDB:", error);
|
||||||
|
// fallback to localStorage
|
||||||
|
try {
|
||||||
|
const elements = localStorage.getItem(
|
||||||
|
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
|
||||||
|
);
|
||||||
|
return elements?.length || 0;
|
||||||
|
} catch (localStorageError: any) {
|
||||||
|
console.error(
|
||||||
|
"Failed to get elements size from localStorage:",
|
||||||
|
localStorageError,
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the total size of all data stored in IndexedDB and localStorage
|
||||||
|
* @returns Promise<number> - Size in bytes
|
||||||
|
*/
|
||||||
|
export const getTotalStorageSize = async () => {
|
||||||
|
try {
|
||||||
|
const appState = await AppStateIndexedDBAdapter.load();
|
||||||
const collab = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
|
const collab = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
|
||||||
|
|
||||||
const appStateSize = appState?.length || 0;
|
const appStateSize = appState ? JSON.stringify(appState).length : 0;
|
||||||
const collabSize = collab?.length || 0;
|
const collabSize = collab?.length || 0;
|
||||||
|
|
||||||
return appStateSize + collabSize + getElementsStorageSize();
|
const elementsSize = await getElementsStorageSize();
|
||||||
|
return appStateSize + collabSize + elementsSize;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error(error);
|
console.error("Failed to get total storage size from IndexedDB:", error);
|
||||||
return 0;
|
// fallback to localStorage
|
||||||
|
try {
|
||||||
|
const appState = localStorage.getItem(
|
||||||
|
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
|
||||||
|
);
|
||||||
|
const collab = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_COLLAB);
|
||||||
|
|
||||||
|
const appStateSize = appState?.length || 0;
|
||||||
|
const collabSize = collab?.length || 0;
|
||||||
|
|
||||||
|
const elementsSize = await getElementsStorageSize();
|
||||||
|
return appStateSize + collabSize + elementsSize;
|
||||||
|
} catch (localStorageError: any) {
|
||||||
|
console.error(
|
||||||
|
"Failed to get total storage size from localStorage:",
|
||||||
|
localStorageError,
|
||||||
|
);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Loading…
x
Reference in New Issue
Block a user