mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 11:00:47 +02:00
Feature/v2/smartfolders rebuild (#6480)
Co-authored-by: aikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
aikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com>
parent
e0fc5061de
commit
e2536daeb8
@@ -0,0 +1,154 @@
|
||||
/**
|
||||
* Service for managing Watched Folder run state in IndexedDB
|
||||
*/
|
||||
|
||||
import { WatchedFolderRunEntry } from "@app/types/watchedFolders";
|
||||
|
||||
const FOLDER_RUN_STATE_CHANGE_EVENT = "folder-run-state-changed";
|
||||
|
||||
interface RunStateRecord {
|
||||
folderId: string;
|
||||
runs: WatchedFolderRunEntry[];
|
||||
lastUpdated: number;
|
||||
}
|
||||
|
||||
class FolderRunStateStorage {
|
||||
private dbName = "stirling-pdf-folder-run-state";
|
||||
private dbVersion = 1;
|
||||
private storeName = "runStates";
|
||||
private db: IDBDatabase | null = null;
|
||||
private initPromise: Promise<void> | null = null;
|
||||
|
||||
async init(): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(this.dbName, this.dbVersion);
|
||||
request.onerror = () =>
|
||||
reject(new Error("Failed to open folder run state database"));
|
||||
request.onsuccess = () => {
|
||||
this.db = request.result;
|
||||
this.db.onclose = () => {
|
||||
this.db = null;
|
||||
this.initPromise = null;
|
||||
};
|
||||
resolve();
|
||||
};
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
if (!db.objectStoreNames.contains(this.storeName)) {
|
||||
db.createObjectStore(this.storeName, { keyPath: "folderId" });
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private async ensureDB(): Promise<IDBDatabase> {
|
||||
if (!this.db) {
|
||||
this.initPromise ??= this.init();
|
||||
await this.initPromise;
|
||||
}
|
||||
if (!this.db) {
|
||||
throw new Error("Folder run state database not initialized");
|
||||
}
|
||||
return this.db;
|
||||
}
|
||||
|
||||
async getFolderRunState(folderId: string): Promise<WatchedFolderRunEntry[]> {
|
||||
const db = await this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([this.storeName], "readonly");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.get(folderId);
|
||||
request.onsuccess = () => {
|
||||
const record: RunStateRecord | undefined = request.result;
|
||||
resolve(record?.runs || []);
|
||||
};
|
||||
request.onerror = () =>
|
||||
reject(new Error("Failed to get folder run state"));
|
||||
});
|
||||
}
|
||||
|
||||
async setFolderRunState(
|
||||
folderId: string,
|
||||
runs: WatchedFolderRunEntry[],
|
||||
): Promise<void> {
|
||||
const db = await this.ensureDB();
|
||||
const record: RunStateRecord = { folderId, runs, lastUpdated: Date.now() };
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([this.storeName], "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.put(record);
|
||||
request.onsuccess = () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(FOLDER_RUN_STATE_CHANGE_EVENT, {
|
||||
detail: { folderId },
|
||||
}),
|
||||
);
|
||||
resolve();
|
||||
};
|
||||
request.onerror = () =>
|
||||
reject(new Error("Failed to set folder run state"));
|
||||
});
|
||||
}
|
||||
|
||||
onRunStateChange(listener: (folderId: string) => void): () => void {
|
||||
const handler = (e: Event) => listener((e as CustomEvent).detail.folderId);
|
||||
window.addEventListener(FOLDER_RUN_STATE_CHANGE_EVENT, handler);
|
||||
return () =>
|
||||
window.removeEventListener(FOLDER_RUN_STATE_CHANGE_EVENT, handler);
|
||||
}
|
||||
|
||||
/** Atomically appends entries to a folder's run state within a single readwrite transaction,
|
||||
* preventing lost-update races when multiple files are processed concurrently. */
|
||||
async appendRunEntries(
|
||||
folderId: string,
|
||||
entries: WatchedFolderRunEntry[],
|
||||
): Promise<void> {
|
||||
if (entries.length === 0) return;
|
||||
const db = await this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([this.storeName], "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const getRequest = store.get(folderId);
|
||||
getRequest.onsuccess = () => {
|
||||
const existing: RunStateRecord | undefined = getRequest.result;
|
||||
const MAX_RUN_ENTRIES = 500;
|
||||
const combined = [...(existing?.runs ?? []), ...entries];
|
||||
const record: RunStateRecord = {
|
||||
folderId,
|
||||
runs:
|
||||
combined.length > MAX_RUN_ENTRIES
|
||||
? combined.slice(-MAX_RUN_ENTRIES)
|
||||
: combined,
|
||||
lastUpdated: Date.now(),
|
||||
};
|
||||
const putRequest = store.put(record);
|
||||
putRequest.onsuccess = () => {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent(FOLDER_RUN_STATE_CHANGE_EVENT, {
|
||||
detail: { folderId },
|
||||
}),
|
||||
);
|
||||
resolve();
|
||||
};
|
||||
putRequest.onerror = () =>
|
||||
reject(new Error("Failed to append run entries"));
|
||||
};
|
||||
getRequest.onerror = () =>
|
||||
reject(new Error("Failed to read run state for append"));
|
||||
});
|
||||
}
|
||||
|
||||
async clearFolderRunState(folderId: string): Promise<void> {
|
||||
const db = await this.ensureDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([this.storeName], "readwrite");
|
||||
const store = transaction.objectStore(this.storeName);
|
||||
const request = store.delete(folderId);
|
||||
request.onsuccess = () => resolve();
|
||||
request.onerror = () =>
|
||||
reject(new Error("Failed to clear folder run state"));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const folderRunStateStorage = new FolderRunStateStorage();
|
||||
Reference in New Issue
Block a user