mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
feat: store library to IndexedDB & support storage adapters (#7655)
This commit is contained in:
parent
480572f893
commit
2382fad4f6
14 changed files with 718 additions and 95 deletions
|
@ -4,6 +4,7 @@ import {
|
|||
LibraryItem,
|
||||
ExcalidrawImperativeAPI,
|
||||
LibraryItemsSource,
|
||||
LibraryItems_anyVersion,
|
||||
} from "../types";
|
||||
import { restoreLibraryItems } from "./restore";
|
||||
import type App from "../components/App";
|
||||
|
@ -23,13 +24,72 @@ import {
|
|||
LIBRARY_SIDEBAR_TAB,
|
||||
} from "../constants";
|
||||
import { libraryItemSvgsCache } from "../hooks/useLibraryItemSvg";
|
||||
import { cloneJSON } from "../utils";
|
||||
import {
|
||||
arrayToMap,
|
||||
cloneJSON,
|
||||
preventUnload,
|
||||
promiseTry,
|
||||
resolvablePromise,
|
||||
} from "../utils";
|
||||
import { MaybePromise } from "../utility-types";
|
||||
import { Emitter } from "../emitter";
|
||||
import { Queue } from "../queue";
|
||||
import { hashElementsVersion, hashString } from "../element";
|
||||
|
||||
type LibraryUpdate = {
|
||||
/** deleted library items since last onLibraryChange event */
|
||||
deletedItems: Map<LibraryItem["id"], LibraryItem>;
|
||||
/** newly added items in the library */
|
||||
addedItems: Map<LibraryItem["id"], LibraryItem>;
|
||||
};
|
||||
|
||||
// an object so that we can later add more properties to it without breaking,
|
||||
// such as schema version
|
||||
export type LibraryPersistedData = { libraryItems: LibraryItems };
|
||||
|
||||
const onLibraryUpdateEmitter = new Emitter<
|
||||
[update: LibraryUpdate, libraryItems: LibraryItems]
|
||||
>();
|
||||
|
||||
export interface LibraryPersistenceAdapter {
|
||||
/**
|
||||
* Should load data that were previously saved into the database using the
|
||||
* `save` method. Should throw if saving fails.
|
||||
*
|
||||
* Will be used internally in multiple places, such as during save to
|
||||
* in order to reconcile changes with latest store data.
|
||||
*/
|
||||
load(metadata: {
|
||||
/**
|
||||
* Priority 1 indicates we're loading latest data with intent
|
||||
* to reconcile with before save.
|
||||
* Priority 2 indicates we're loading for read-only purposes, so
|
||||
* host app can implement more aggressive caching strategy.
|
||||
*/
|
||||
priority: 1 | 2;
|
||||
}): MaybePromise<{ libraryItems: LibraryItems_anyVersion } | null>;
|
||||
/** Should persist to the database as is (do no change the data structure). */
|
||||
save(libraryData: LibraryPersistedData): MaybePromise<void>;
|
||||
}
|
||||
|
||||
export interface LibraryMigrationAdapter {
|
||||
/**
|
||||
* loads data from legacy data source. Returns `null` if no data is
|
||||
* to be migrated.
|
||||
*/
|
||||
load(): MaybePromise<{ libraryItems: LibraryItems_anyVersion } | null>;
|
||||
|
||||
/** clears entire storage afterwards */
|
||||
clear(): MaybePromise<void>;
|
||||
}
|
||||
|
||||
export const libraryItemsAtom = atom<{
|
||||
status: "loading" | "loaded";
|
||||
/** indicates whether library is initialized with library items (has gone
|
||||
* through at least one update). Used in UI. Specific to this atom only. */
|
||||
isInitialized: boolean;
|
||||
libraryItems: LibraryItems;
|
||||
}>({ status: "loaded", isInitialized: true, libraryItems: [] });
|
||||
}>({ status: "loaded", isInitialized: false, libraryItems: [] });
|
||||
|
||||
const cloneLibraryItems = (libraryItems: LibraryItems): LibraryItems =>
|
||||
cloneJSON(libraryItems);
|
||||
|
@ -74,12 +134,45 @@ export const mergeLibraryItems = (
|
|||
return [...newItems, ...localItems];
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns { deletedItems, addedItems } maps of all added and deleted items
|
||||
* since last onLibraryChange event.
|
||||
*
|
||||
* Host apps are recommended to diff with the latest state they have.
|
||||
*/
|
||||
const createLibraryUpdate = (
|
||||
prevLibraryItems: LibraryItems,
|
||||
nextLibraryItems: LibraryItems,
|
||||
): LibraryUpdate => {
|
||||
const nextItemsMap = arrayToMap(nextLibraryItems);
|
||||
|
||||
const update: LibraryUpdate = {
|
||||
deletedItems: new Map<LibraryItem["id"], LibraryItem>(),
|
||||
addedItems: new Map<LibraryItem["id"], LibraryItem>(),
|
||||
};
|
||||
|
||||
for (const item of prevLibraryItems) {
|
||||
if (!nextItemsMap.has(item.id)) {
|
||||
update.deletedItems.set(item.id, item);
|
||||
}
|
||||
}
|
||||
|
||||
const prevItemsMap = arrayToMap(prevLibraryItems);
|
||||
|
||||
for (const item of nextLibraryItems) {
|
||||
if (!prevItemsMap.has(item.id)) {
|
||||
update.addedItems.set(item.id, item);
|
||||
}
|
||||
}
|
||||
|
||||
return update;
|
||||
};
|
||||
|
||||
class Library {
|
||||
/** latest libraryItems */
|
||||
private lastLibraryItems: LibraryItems = [];
|
||||
/** indicates whether library is initialized with library items (has gone
|
||||
* though at least one update) */
|
||||
private isInitialized = false;
|
||||
private currLibraryItems: LibraryItems = [];
|
||||
/** snapshot of library items since last onLibraryChange call */
|
||||
private prevLibraryItems = cloneLibraryItems(this.currLibraryItems);
|
||||
|
||||
private app: App;
|
||||
|
||||
|
@ -95,21 +188,29 @@ class Library {
|
|||
|
||||
private notifyListeners = () => {
|
||||
if (this.updateQueue.length > 0) {
|
||||
jotaiStore.set(libraryItemsAtom, {
|
||||
jotaiStore.set(libraryItemsAtom, (s) => ({
|
||||
status: "loading",
|
||||
libraryItems: this.lastLibraryItems,
|
||||
isInitialized: this.isInitialized,
|
||||
});
|
||||
libraryItems: this.currLibraryItems,
|
||||
isInitialized: s.isInitialized,
|
||||
}));
|
||||
} else {
|
||||
this.isInitialized = true;
|
||||
jotaiStore.set(libraryItemsAtom, {
|
||||
status: "loaded",
|
||||
libraryItems: this.lastLibraryItems,
|
||||
isInitialized: this.isInitialized,
|
||||
libraryItems: this.currLibraryItems,
|
||||
isInitialized: true,
|
||||
});
|
||||
try {
|
||||
this.app.props.onLibraryChange?.(
|
||||
cloneLibraryItems(this.lastLibraryItems),
|
||||
const prevLibraryItems = this.prevLibraryItems;
|
||||
this.prevLibraryItems = cloneLibraryItems(this.currLibraryItems);
|
||||
|
||||
const nextLibraryItems = cloneLibraryItems(this.currLibraryItems);
|
||||
|
||||
this.app.props.onLibraryChange?.(nextLibraryItems);
|
||||
|
||||
// for internal use in `useHandleLibrary` hook
|
||||
onLibraryUpdateEmitter.trigger(
|
||||
createLibraryUpdate(prevLibraryItems, nextLibraryItems),
|
||||
nextLibraryItems,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
@ -119,9 +220,8 @@ class Library {
|
|||
|
||||
/** call on excalidraw instance unmount */
|
||||
destroy = () => {
|
||||
this.isInitialized = false;
|
||||
this.updateQueue = [];
|
||||
this.lastLibraryItems = [];
|
||||
this.currLibraryItems = [];
|
||||
jotaiStore.set(libraryItemSvgsCache, new Map());
|
||||
// TODO uncomment after/if we make jotai store scoped to each excal instance
|
||||
// jotaiStore.set(libraryItemsAtom, {
|
||||
|
@ -142,14 +242,14 @@ class Library {
|
|||
return new Promise(async (resolve) => {
|
||||
try {
|
||||
const libraryItems = await (this.getLastUpdateTask() ||
|
||||
this.lastLibraryItems);
|
||||
this.currLibraryItems);
|
||||
if (this.updateQueue.length > 0) {
|
||||
resolve(this.getLatestLibrary());
|
||||
} else {
|
||||
resolve(cloneLibraryItems(libraryItems));
|
||||
}
|
||||
} catch (error) {
|
||||
return resolve(this.lastLibraryItems);
|
||||
return resolve(this.currLibraryItems);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -181,7 +281,7 @@ class Library {
|
|||
try {
|
||||
const source = await (typeof libraryItems === "function" &&
|
||||
!(libraryItems instanceof Blob)
|
||||
? libraryItems(this.lastLibraryItems)
|
||||
? libraryItems(this.currLibraryItems)
|
||||
: libraryItems);
|
||||
|
||||
let nextItems;
|
||||
|
@ -207,7 +307,7 @@ class Library {
|
|||
}
|
||||
|
||||
if (merge) {
|
||||
resolve(mergeLibraryItems(this.lastLibraryItems, nextItems));
|
||||
resolve(mergeLibraryItems(this.currLibraryItems, nextItems));
|
||||
} else {
|
||||
resolve(nextItems);
|
||||
}
|
||||
|
@ -244,12 +344,12 @@ class Library {
|
|||
await this.getLastUpdateTask();
|
||||
|
||||
if (typeof libraryItems === "function") {
|
||||
libraryItems = libraryItems(this.lastLibraryItems);
|
||||
libraryItems = libraryItems(this.currLibraryItems);
|
||||
}
|
||||
|
||||
this.lastLibraryItems = cloneLibraryItems(await libraryItems);
|
||||
this.currLibraryItems = cloneLibraryItems(await libraryItems);
|
||||
|
||||
resolve(this.lastLibraryItems);
|
||||
resolve(this.currLibraryItems);
|
||||
} catch (error: any) {
|
||||
reject(error);
|
||||
}
|
||||
|
@ -257,7 +357,7 @@ class Library {
|
|||
.catch((error) => {
|
||||
if (error.name === "AbortError") {
|
||||
console.warn("Library update aborted by user");
|
||||
return this.lastLibraryItems;
|
||||
return this.currLibraryItems;
|
||||
}
|
||||
throw error;
|
||||
})
|
||||
|
@ -382,20 +482,165 @@ export const parseLibraryTokensFromUrl = () => {
|
|||
return libraryUrl ? { libraryUrl, idToken } : null;
|
||||
};
|
||||
|
||||
export const useHandleLibrary = ({
|
||||
excalidrawAPI,
|
||||
getInitialLibraryItems,
|
||||
}: {
|
||||
excalidrawAPI: ExcalidrawImperativeAPI | null;
|
||||
getInitialLibraryItems?: () => LibraryItemsSource;
|
||||
}) => {
|
||||
const getInitialLibraryRef = useRef(getInitialLibraryItems);
|
||||
class AdapterTransaction {
|
||||
static queue = new Queue();
|
||||
|
||||
static async getLibraryItems(
|
||||
adapter: LibraryPersistenceAdapter,
|
||||
priority: 1 | 2,
|
||||
_queue = true,
|
||||
): Promise<LibraryItems> {
|
||||
const task = () =>
|
||||
new Promise<LibraryItems>(async (resolve, reject) => {
|
||||
try {
|
||||
const data = await adapter.load({ priority });
|
||||
resolve(restoreLibraryItems(data?.libraryItems || [], "published"));
|
||||
} catch (error: any) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
if (_queue) {
|
||||
return AdapterTransaction.queue.push(task);
|
||||
}
|
||||
|
||||
return task();
|
||||
}
|
||||
|
||||
static run = async <T>(
|
||||
adapter: LibraryPersistenceAdapter,
|
||||
fn: (transaction: AdapterTransaction) => Promise<T>,
|
||||
) => {
|
||||
const transaction = new AdapterTransaction(adapter);
|
||||
return AdapterTransaction.queue.push(() => fn(transaction));
|
||||
};
|
||||
|
||||
// ------------------
|
||||
|
||||
private adapter: LibraryPersistenceAdapter;
|
||||
|
||||
constructor(adapter: LibraryPersistenceAdapter) {
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
getLibraryItems(priority: 1 | 2) {
|
||||
return AdapterTransaction.getLibraryItems(this.adapter, priority, false);
|
||||
}
|
||||
}
|
||||
|
||||
let lastSavedLibraryItemsHash = 0;
|
||||
let librarySaveCounter = 0;
|
||||
|
||||
export const getLibraryItemsHash = (items: LibraryItems) => {
|
||||
return hashString(
|
||||
items
|
||||
.map((item) => {
|
||||
return `${item.id}:${hashElementsVersion(item.elements)}`;
|
||||
})
|
||||
.sort()
|
||||
.join(),
|
||||
);
|
||||
};
|
||||
|
||||
const persistLibraryUpdate = async (
|
||||
adapter: LibraryPersistenceAdapter,
|
||||
update: LibraryUpdate,
|
||||
): Promise<LibraryItems> => {
|
||||
try {
|
||||
librarySaveCounter++;
|
||||
|
||||
return await AdapterTransaction.run(adapter, async (transaction) => {
|
||||
const nextLibraryItemsMap = arrayToMap(
|
||||
await transaction.getLibraryItems(1),
|
||||
);
|
||||
|
||||
for (const [id] of update.deletedItems) {
|
||||
nextLibraryItemsMap.delete(id);
|
||||
}
|
||||
|
||||
const addedItems: LibraryItem[] = [];
|
||||
|
||||
// we want to merge current library items with the ones stored in the
|
||||
// DB so that we don't lose any elements that for some reason aren't
|
||||
// in the current editor library, which could happen when:
|
||||
//
|
||||
// 1. we haven't received an update deleting some elements
|
||||
// (in which case it's still better to keep them in the DB lest
|
||||
// it was due to a different reason)
|
||||
// 2. we keep a single DB for all active editors, but the editors'
|
||||
// libraries aren't synced or there's a race conditions during
|
||||
// syncing
|
||||
// 3. some other race condition, e.g. during init where emit updates
|
||||
// for partial updates (e.g. you install a 3rd party library and
|
||||
// init from DB only after — we emit events for both updates)
|
||||
for (const [id, item] of update.addedItems) {
|
||||
if (nextLibraryItemsMap.has(id)) {
|
||||
// replace item with latest version
|
||||
// TODO we could prefer the newer item instead
|
||||
nextLibraryItemsMap.set(id, item);
|
||||
} else {
|
||||
// we want to prepend the new items with the ones that are already
|
||||
// in DB to preserve the ordering we do in editor (newly added
|
||||
// items are added to the beginning)
|
||||
addedItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
const nextLibraryItems = addedItems.concat(
|
||||
Array.from(nextLibraryItemsMap.values()),
|
||||
);
|
||||
|
||||
const version = getLibraryItemsHash(nextLibraryItems);
|
||||
|
||||
if (version !== lastSavedLibraryItemsHash) {
|
||||
await adapter.save({ libraryItems: nextLibraryItems });
|
||||
}
|
||||
|
||||
lastSavedLibraryItemsHash = version;
|
||||
|
||||
return nextLibraryItems;
|
||||
});
|
||||
} finally {
|
||||
librarySaveCounter--;
|
||||
}
|
||||
};
|
||||
|
||||
export const useHandleLibrary = (
|
||||
opts: {
|
||||
excalidrawAPI: ExcalidrawImperativeAPI | null;
|
||||
} & (
|
||||
| {
|
||||
/** @deprecated we recommend using `opts.adapter` instead */
|
||||
getInitialLibraryItems?: () => MaybePromise<LibraryItemsSource>;
|
||||
}
|
||||
| {
|
||||
adapter: LibraryPersistenceAdapter;
|
||||
/**
|
||||
* Adapter that takes care of loading data from legacy data store.
|
||||
* Supply this if you want to migrate data on initial load from legacy
|
||||
* data store.
|
||||
*
|
||||
* Can be a different LibraryPersistenceAdapter.
|
||||
*/
|
||||
migrationAdapter?: LibraryMigrationAdapter;
|
||||
}
|
||||
),
|
||||
) => {
|
||||
const { excalidrawAPI } = opts;
|
||||
|
||||
const optsRef = useRef(opts);
|
||||
optsRef.current = opts;
|
||||
|
||||
const isLibraryLoadedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!excalidrawAPI) {
|
||||
return;
|
||||
}
|
||||
|
||||
// reset on editor remount (excalidrawAPI changed)
|
||||
isLibraryLoadedRef.current = false;
|
||||
|
||||
const importLibraryFromURL = async ({
|
||||
libraryUrl,
|
||||
idToken,
|
||||
|
@ -463,23 +708,209 @@ export const useHandleLibrary = ({
|
|||
};
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// ------ init load --------------------------------------------------------
|
||||
if (getInitialLibraryRef.current) {
|
||||
excalidrawAPI.updateLibrary({
|
||||
libraryItems: getInitialLibraryRef.current(),
|
||||
});
|
||||
}
|
||||
// ---------------------------------- init ---------------------------------
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
const libraryUrlTokens = parseLibraryTokensFromUrl();
|
||||
|
||||
if (libraryUrlTokens) {
|
||||
importLibraryFromURL(libraryUrlTokens);
|
||||
}
|
||||
|
||||
// ------ (A) init load (legacy) -------------------------------------------
|
||||
if (
|
||||
"getInitialLibraryItems" in optsRef.current &&
|
||||
optsRef.current.getInitialLibraryItems
|
||||
) {
|
||||
console.warn(
|
||||
"useHandleLibrar `opts.getInitialLibraryItems` is deprecated. Use `opts.adapter` instead.",
|
||||
);
|
||||
|
||||
Promise.resolve(optsRef.current.getInitialLibraryItems())
|
||||
.then((libraryItems) => {
|
||||
excalidrawAPI.updateLibrary({
|
||||
libraryItems,
|
||||
// merge with current library items because we may have already
|
||||
// populated it (e.g. by installing 3rd party library which can
|
||||
// happen before the DB data is loaded)
|
||||
merge: true,
|
||||
});
|
||||
})
|
||||
.catch((error: any) => {
|
||||
console.error(
|
||||
`UseHandeLibrary getInitialLibraryItems failed: ${error?.message}`,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// --------------------------------------------------------- init load -----
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
// ------ (B) data source adapter ------------------------------------------
|
||||
|
||||
if ("adapter" in optsRef.current && optsRef.current.adapter) {
|
||||
const adapter = optsRef.current.adapter;
|
||||
const migrationAdapter = optsRef.current.migrationAdapter;
|
||||
|
||||
const initDataPromise = resolvablePromise<LibraryItems | null>();
|
||||
|
||||
// migrate from old data source if needed
|
||||
// (note, if `migrate` function is defined, we always migrate even
|
||||
// if the data has already been migrated. In that case it'll be a no-op,
|
||||
// though with several unnecessary steps — we will still load latest
|
||||
// DB data during the `persistLibraryChange()` step)
|
||||
// -----------------------------------------------------------------------
|
||||
if (migrationAdapter) {
|
||||
initDataPromise.resolve(
|
||||
promiseTry(migrationAdapter.load)
|
||||
.then(async (libraryData) => {
|
||||
try {
|
||||
// if no library data to migrate, assume no migration needed
|
||||
// and skip persisting to new data store, as well as well
|
||||
// clearing the old store via `migrationAdapter.clear()`
|
||||
if (!libraryData) {
|
||||
return AdapterTransaction.getLibraryItems(adapter, 2);
|
||||
}
|
||||
|
||||
// we don't queue this operation because it's running inside
|
||||
// a promise that's running inside Library update queue itself
|
||||
const nextItems = await persistLibraryUpdate(
|
||||
adapter,
|
||||
createLibraryUpdate(
|
||||
[],
|
||||
restoreLibraryItems(
|
||||
libraryData.libraryItems || [],
|
||||
"published",
|
||||
),
|
||||
),
|
||||
);
|
||||
try {
|
||||
await migrationAdapter.clear();
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
`couldn't delete legacy library data: ${error.message}`,
|
||||
);
|
||||
}
|
||||
// migration suceeded, load migrated data
|
||||
return nextItems;
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
`couldn't migrate legacy library data: ${error.message}`,
|
||||
);
|
||||
// migration failed, load empty library
|
||||
return [];
|
||||
}
|
||||
})
|
||||
// errors caught during `migrationAdapter.load()`
|
||||
.catch((error: any) => {
|
||||
console.error(`error during library migration: ${error.message}`);
|
||||
// as a default, load latest library from current data source
|
||||
return AdapterTransaction.getLibraryItems(adapter, 2);
|
||||
}),
|
||||
);
|
||||
} else {
|
||||
initDataPromise.resolve(
|
||||
promiseTry(AdapterTransaction.getLibraryItems, adapter, 2),
|
||||
);
|
||||
}
|
||||
|
||||
// load initial (or migrated) library
|
||||
excalidrawAPI
|
||||
.updateLibrary({
|
||||
libraryItems: initDataPromise.then((libraryItems) => {
|
||||
const _libraryItems = libraryItems || [];
|
||||
lastSavedLibraryItemsHash = getLibraryItemsHash(_libraryItems);
|
||||
return _libraryItems;
|
||||
}),
|
||||
// merge with current library items because we may have already
|
||||
// populated it (e.g. by installing 3rd party library which can
|
||||
// happen before the DB data is loaded)
|
||||
merge: true,
|
||||
})
|
||||
.finally(() => {
|
||||
isLibraryLoadedRef.current = true;
|
||||
});
|
||||
}
|
||||
// ---------------------------------------------- data source datapter -----
|
||||
|
||||
window.addEventListener(EVENT.HASHCHANGE, onHashChange);
|
||||
return () => {
|
||||
window.removeEventListener(EVENT.HASHCHANGE, onHashChange);
|
||||
};
|
||||
}, [excalidrawAPI]);
|
||||
}, [
|
||||
// important this useEffect only depends on excalidrawAPI so it only reruns
|
||||
// on editor remounts (the excalidrawAPI changes)
|
||||
excalidrawAPI,
|
||||
]);
|
||||
|
||||
// This effect is run without excalidrawAPI dependency so that host apps
|
||||
// can run this hook outside of an active editor instance and the library
|
||||
// update queue/loop survives editor remounts
|
||||
//
|
||||
// This effect is still only meant to be run if host apps supply an persitence
|
||||
// adapter. If we don't have access to it, it the update listener doesn't
|
||||
// do anything.
|
||||
useEffect(
|
||||
() => {
|
||||
// on update, merge with current library items and persist
|
||||
// -----------------------------------------------------------------------
|
||||
const unsubOnLibraryUpdate = onLibraryUpdateEmitter.on(
|
||||
async (update, nextLibraryItems) => {
|
||||
const isLoaded = isLibraryLoadedRef.current;
|
||||
// we want to operate with the latest adapter, but we don't want this
|
||||
// effect to rerun on every adapter change in case host apps' adapter
|
||||
// isn't stable
|
||||
const adapter =
|
||||
("adapter" in optsRef.current && optsRef.current.adapter) || null;
|
||||
try {
|
||||
if (adapter) {
|
||||
if (
|
||||
// if nextLibraryItems hash identical to previously saved hash,
|
||||
// exit early, even if actual upstream state ends up being
|
||||
// different (e.g. has more data than we have locally), as it'd
|
||||
// be low-impact scenario.
|
||||
lastSavedLibraryItemsHash !==
|
||||
getLibraryItemsHash(nextLibraryItems)
|
||||
) {
|
||||
await persistLibraryUpdate(adapter, update);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(
|
||||
`couldn't persist library update: ${error.message}`,
|
||||
update,
|
||||
);
|
||||
|
||||
// currently we only show error if an editor is loaded
|
||||
if (isLoaded && optsRef.current.excalidrawAPI) {
|
||||
optsRef.current.excalidrawAPI.updateScene({
|
||||
appState: {
|
||||
errorMessage: t("errors.saveLibraryError"),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const onUnload = (event: Event) => {
|
||||
if (librarySaveCounter) {
|
||||
preventUnload(event);
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener(EVENT.BEFORE_UNLOAD, onUnload);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener(EVENT.BEFORE_UNLOAD, onUnload);
|
||||
unsubOnLibraryUpdate();
|
||||
lastSavedLibraryItemsHash = 0;
|
||||
librarySaveCounter = 0;
|
||||
};
|
||||
},
|
||||
[
|
||||
// this effect must not have any deps so it doesn't rerun
|
||||
],
|
||||
);
|
||||
};
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue