fix: use idb for elements and app state

This commit is contained in:
Ryan Di
2025-07-31 18:21:56 +10:00
parent 37ad85cbaf
commit cff7516318
4 changed files with 238 additions and 30 deletions
+69 -11
View File
@@ -65,7 +65,7 @@ class LocalFileManager extends FileManager {
};
}
const saveDataStateToLocalStorage = (
const saveDataStateToIndexedDB = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
@@ -79,17 +79,15 @@ const saveDataStateToLocalStorage = (
_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),
);
// save to IndexedDB
await Promise.all([
ElementsIndexedDBAdapter.save(clearElementsForLocalStorage(elements)),
AppStateIndexedDBAdapter.save(_appState),
]);
updateBrowserStateVersion(STORAGE_KEYS.VERSION_DATA_STATE);
} catch (error: any) {
// Unable to access window.localStorage
// unable to access IndexedDB
console.error(error);
}
};
@@ -104,7 +102,7 @@ export class LocalData {
files: BinaryFiles,
onFilesSaved: () => void,
) => {
saveDataStateToLocalStorage(elements, appState);
await saveDataStateToIndexedDB(elements, appState);
await this.fileStorage.saveFiles({
elements,
@@ -256,3 +254,63 @@ export class LibraryLocalStorageMigrationAdapter {
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,
);
}
}
+134 -11
View File
@@ -9,6 +9,11 @@ import type { AppState } from "@excalidraw/excalidraw/types";
import { STORAGE_KEYS } from "../app_constants";
import {
AppStateIndexedDBAdapter,
ElementsIndexedDBAdapter,
} from "./LocalData";
export const saveUsernameToLocalStorage = (username: string) => {
try {
localStorage.setItem(
@@ -74,28 +79,146 @@ export const importFromLocalStorage = () => {
return { elements, appState };
};
export const getElementsStorageSize = () => {
export const importFromIndexedDB = async () => {
let savedElements = null;
let savedState = null;
try {
const elements = localStorage.getItem(STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS);
const elementsSize = elements?.length || 0;
return elementsSize;
savedElements = await ElementsIndexedDBAdapter.load();
savedState = await AppStateIndexedDBAdapter.load();
} catch (error: any) {
// unable to access IndexedDB
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 {
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 appStateSize = appState?.length || 0;
const appStateSize = appState ? JSON.stringify(appState).length : 0;
const collabSize = collab?.length || 0;
return appStateSize + collabSize + getElementsStorageSize();
const elementsSize = await getElementsStorageSize();
return appStateSize + collabSize + elementsSize;
} catch (error: any) {
console.error(error);
return 0;
console.error("Failed to get total storage size from IndexedDB:", error);
// 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;
}
}
};