This commit is contained in:
zsviczian
2023-07-02 15:43:58 +02:00
parent 2f77988473
commit 4f726cbcd0
13 changed files with 477 additions and 150 deletions

View File

@@ -19,22 +19,26 @@ type ImageKey = {
scale: number;
};
const getKey = (key: ImageKey): string => `${key.filepath}#${key.blockref}#${key.sectionref}#${key.isDark?1:0}#${key.isSVG?1:0}#${key.scale}`;
const getKey = (key: ImageKey): string =>
`${key.filepath}#${key.blockref}#${key.sectionref}#${key.isDark ? 1 : 0}#${key.isSVG ? 1 : 0}#${key.scale}`;
class ImageCache {
private dbName: string;
private cacheStoreName: string;
private backupStoreName: string;
private db: IDBDatabase | null;
private isInitializing: boolean;
public plugin: ExcalidrawPlugin;
public initializationNotice: boolean = false;
constructor(dbName: string, cacheStoreName: string) {
constructor(dbName: string, cacheStoreName: string, backupStoreName: string) {
this.dbName = dbName;
this.cacheStoreName = cacheStoreName;
this.backupStoreName = backupStoreName;
this.db = null;
this.isInitializing = false;
this.plugin = null;
app.workspace.onLayoutReady(()=>this.initializeDB());
app.workspace.onLayoutReady(() => this.initializeDB());
}
private async initializeDB(): Promise<void> {
@@ -52,6 +56,9 @@ class ImageCache {
if (!db.objectStoreNames.contains(this.cacheStoreName)) {
db.createObjectStore(this.cacheStoreName);
}
if (!db.objectStoreNames.contains(this.backupStoreName)) {
db.createObjectStore(this.backupStoreName);
}
};
this.db = await new Promise<IDBDatabase>((resolve, reject) => {
@@ -64,21 +71,29 @@ class ImageCache {
reject(new Error(`Failed to open or create IndexedDB database: ${this.dbName}`));
};
});
// Pre-create the object store to reduce delay when accessing it later
if (!this.db.objectStoreNames.contains(this.cacheStoreName)) {
// Pre-create the object stores to reduce delay when accessing them later
if (
!this.db.objectStoreNames.contains(this.cacheStoreName) ||
!this.db.objectStoreNames.contains(this.backupStoreName)
) {
const version = this.db.version + 1;
this.db.close();
const upgradeRequest = indexedDB.open(this.dbName, version);
upgradeRequest.onupgradeneeded = (event: IDBVersionChangeEvent) => {
const db = (event.target as IDBOpenDBRequest).result;
db.createObjectStore(this.cacheStoreName);
if (!db.objectStoreNames.contains(this.cacheStoreName)) {
db.createObjectStore(this.cacheStoreName);
}
if (!db.objectStoreNames.contains(this.backupStoreName)) {
db.createObjectStore(this.backupStoreName);
}
};
await new Promise<void>((resolve, reject) => {
upgradeRequest.onsuccess = () => {
const db = (upgradeRequest.result as IDBDatabase);
const db = upgradeRequest.result as IDBDatabase;
db.close();
resolve();
};
@@ -91,7 +106,7 @@ class ImageCache {
this.db = await new Promise<IDBDatabase>((resolve, reject) => {
const openRequest = indexedDB.open(this.dbName);
openRequest.onsuccess = () => {
const db = (openRequest.result as IDBDatabase);
const db = openRequest.result as IDBDatabase;
resolve(db);
};
openRequest.onerror = () => {
@@ -99,12 +114,17 @@ class ImageCache {
};
});
}
await this.purgeInvalidCacheFiles();
await this.purgeInvalidCacheFiles();
await this.purgeInvalidBackupFiles();
} finally {
this.isInitializing = false;
if(this.initializationNotice) {
new Notice("Excalidraw Image Cache is Initialized - You may now retry opening your damaged drawing.");
this.initializationNotice = false;
}
console.log("Initialized Excalidraw Image Cache");
}
}
}
private async purgeInvalidCacheFiles(): Promise<void> {
@@ -147,13 +167,51 @@ class ImageCache {
});
}
private async getObjectStore(mode: IDBTransactionMode): Promise<IDBObjectStore> {
const transaction = this.db!.transaction(this.cacheStoreName, mode);
return transaction.objectStore(this.cacheStoreName);
private async purgeInvalidBackupFiles(): Promise<void> {
const transaction = this.db!.transaction(this.backupStoreName, "readwrite");
const store = transaction.objectStore(this.backupStoreName);
const files = app.vault.getFiles();
const deletePromises: Promise<void>[] = [];
const request = store.openCursor();
return new Promise<void>((resolve, reject) => {
request.onsuccess = (event: Event) => {
const cursor = (event.target as IDBRequest<IDBCursorWithValue | null>).result;
if (cursor) {
const key = cursor.key as BackupKey;
const fileExists = files.some((f: TFile) => f.path === key);
if (!fileExists) {
deletePromises.push(
new Promise<void>((resolve, reject) => {
const deleteRequest = store.delete(cursor.primaryKey);
deleteRequest.onsuccess = () => resolve();
deleteRequest.onerror = () =>
reject(new Error(`Failed to delete backup file with key: ${key}`));
})
);
}
cursor.continue();
} else {
Promise.all(deletePromises)
.then(() => resolve())
.catch((error) => reject(error));
}
};
request.onerror = () => {
reject(new Error("Failed to purge invalid backup files from IndexedDB."));
};
});
}
private async getObjectStore(mode: IDBTransactionMode, storeName: string): Promise<IDBObjectStore> {
const transaction = this.db!.transaction(storeName, mode);
return transaction.objectStore(storeName);
}
private async getCacheData(key: string): Promise<FileCacheData | null> {
const store = await this.getObjectStore("readonly");
const store = await this.getObjectStore("readonly", this.cacheStoreName);
const request = store.get(key);
return new Promise<FileCacheData | null>((resolve, reject) => {
@@ -168,9 +226,25 @@ class ImageCache {
});
}
private async getBackupData(key: BackupKey): Promise<BackupData | null> {
const store = await this.getObjectStore("readonly", this.backupStoreName);
const request = store.get(key);
return new Promise<BackupData | null>((resolve, reject) => {
request.onsuccess = () => {
const result = request.result as BackupData;
resolve(result || null);
};
request.onerror = () => {
reject(new Error("Failed to retrieve backup data from IndexedDB."));
};
});
}
public isReady(): boolean {
return !!this.db && !this.isInitializing && !!this.plugin && this.plugin.settings.allowImageCache;
}
}
public async getImageFromCache(key_: ImageKey): Promise<string | undefined> {
if (!this.isReady()) {
@@ -188,9 +262,17 @@ class ImageCache {
});
}
public async getBAKFromCache(filepath: string): Promise<BackupData | null> {
if (!this.isReady()) {
return null; // Database not initialized yet
}
return this.getBackupData(filepath);
}
public addImageToCache(key_: ImageKey, imageBase64: string): void {
if (!this.isReady()) {
return; // Database not initialized yet
return; // Database not initialized yet
}
const file = app.vault.getAbstractFileByPath(key_.filepath.split("#")[0]);
@@ -199,31 +281,54 @@ class ImageCache {
const transaction = this.db.transaction(this.cacheStoreName, "readwrite");
const store = transaction.objectStore(this.cacheStoreName);
const key = getKey(key_)
const key = getKey(key_);
store.put(data, key);
}
public async clear(): Promise<void> {
// deliberately not checking isReady() here
if (!this.db || this.isInitializing) {
public async addBAKToCache(filepath: string, data: BackupData): Promise<void> {
if (!this.isReady()) {
return; // Database not initialized yet
}
const transaction = this.db.transaction(this.cacheStoreName, "readwrite");
const store = transaction.objectStore(this.cacheStoreName);
const request = store.clear();
const transaction = this.db.transaction(this.backupStoreName, "readwrite");
const store = transaction.objectStore(this.backupStoreName);
store.put(data, filepath);
}
public async clearImageCache(): Promise<void> {
if (!this.isReady()) {
return; // Database not initialized yet
}
return this.clear(this.cacheStoreName, "Image cache was cleared");
}
public async clearBackupCache(): Promise<void> {
if (!this.isReady()) {
return; // Database not initialized yet
}
return this.clear(this.backupStoreName, "All backups were cleared");
}
private async clear(storeName: string, message: string): Promise<void> {
if (!this.isReady()) {
return; // Database not initialized yet
}
const transaction = this.db.transaction([storeName], "readwrite");
const store = transaction.objectStore(storeName);
return new Promise<void>((resolve, reject) => {
const request = store.clear();
request.onsuccess = () => {
new Notice("Image cache cleared.");
new Notice(message);
resolve();
};
request.onerror = () => {
reject(new Error("Failed to clear data in IndexedDB."));
};
request.onerror = () => reject(new Error(`Failed to clear ${storeName}.`));
});
}
}
export const imageCache = new ImageCache(DB_NAME, CACHE_STORE);
export const imageCache = new ImageCache(DB_NAME, CACHE_STORE, BACKUP_STORE);