Files
Stirling-PDF/frontend/editor/src/proprietary/services/folderRunStateStorage.ts
T
Reece BrowneGitHubaikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com>
e2536daeb8 Feature/v2/smartfolders rebuild (#6480)
Co-authored-by: aikido-pr-checks[bot] <169896070+aikido-pr-checks[bot]@users.noreply.github.com>
2026-06-10 11:18:14 +01:00

155 lines
5.2 KiB
TypeScript

/**
* 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();