import { get } from "svelte/store"; import type { PortfolioStore } from "/src/stores/portfolio"; import type { MessageBody } from "/src/subscriptions-router"; import type { EditorWrapper } from "/wrapper/pkg/graphite_wasm_wrapper"; const PERSISTENCE_DB = "graphite"; const PERSISTENCE_STORE = "store"; export async function storeDocumentTabOrder(portfolio: PortfolioStore) { const documentOrder = get(portfolio).documents.map((doc) => String(doc.id)); await databaseSet("documents_tab_order", documentOrder); } export async function storeCurrentDocumentId(documentId: string) { await databaseSet("current_document_id", String(documentId)); } export async function storeDocument(autoSaveDocument: MessageBody<"TriggerPersistenceWriteDocument">, portfolio: PortfolioStore) { await databaseUpdate>>("documents", (old) => { const documents = old || {}; documents[String(autoSaveDocument.documentId)] = autoSaveDocument; return documents; }); await storeDocumentTabOrder(portfolio); await storeCurrentDocumentId(String(autoSaveDocument.documentId)); } export async function removeDocument(id: string, portfolio: PortfolioStore) { await databaseUpdate>>("documents", (old) => { const documents = old || {}; delete documents[id]; return documents; }); await databaseUpdate("documents_tab_order", (old) => { const order = old || []; return order.filter((docId) => docId !== id); }); const documentCount = get(portfolio).documents.length; if (documentCount > 0) { const documentIndex = get(portfolio).activeDocumentIndex; const documentId = String(get(portfolio).documents[documentIndex].id); const tabOrder = (await databaseGet("documents_tab_order")) || []; if (tabOrder.includes(documentId)) { await storeCurrentDocumentId(documentId); } } else { await databaseDelete("current_document_id"); } } export async function loadFirstDocument(editor: EditorWrapper) { const previouslySavedDocuments = await databaseGet>>("documents"); // TODO: Eventually remove this document upgrade code // Migrate TriggerPersistenceWriteDocument.documentId from string to bigint if the browser is storing the old format as strings if (previouslySavedDocuments) { Object.values(previouslySavedDocuments).forEach((doc) => { if (typeof doc.documentId === "string") doc.documentId = BigInt(doc.documentId); }); } const documentOrder = await databaseGet("documents_tab_order"); const currentDocumentIdString = await databaseGet("current_document_id"); const currentDocumentId = currentDocumentIdString ? BigInt(currentDocumentIdString) : undefined; if (!previouslySavedDocuments || !documentOrder) return; const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : [])); if (currentDocumentId !== undefined && String(currentDocumentId) in previouslySavedDocuments) { const doc = previouslySavedDocuments[String(currentDocumentId)]; editor.openAutoSavedDocument(doc.documentId, doc.details.name, doc.details.isSaved, doc.document, false); editor.selectDocument(currentDocumentId); } else { const len = orderedSavedDocuments.length; if (len > 0) { const doc = orderedSavedDocuments[len - 1]; editor.openAutoSavedDocument(doc.documentId, doc.details.name, doc.details.isSaved, doc.document, false); editor.selectDocument(doc.documentId); } } } export async function loadRestDocuments(editor: EditorWrapper) { const previouslySavedDocuments = await databaseGet>>("documents"); // TODO: Eventually remove this document upgrade code // Migrate TriggerPersistenceWriteDocument.documentId from string to bigint if needed if (previouslySavedDocuments) { Object.values(previouslySavedDocuments).forEach((doc) => { if (typeof doc.documentId === "string") doc.documentId = BigInt(doc.documentId); }); } const documentOrder = await databaseGet("documents_tab_order"); const currentDocumentIdString = await databaseGet("current_document_id"); const currentDocumentId = currentDocumentIdString ? BigInt(currentDocumentIdString) : undefined; if (!previouslySavedDocuments || !documentOrder) return; const orderedSavedDocuments = documentOrder.flatMap((id) => (previouslySavedDocuments[id] ? [previouslySavedDocuments[id]] : [])); const currentIndex = currentDocumentId !== undefined ? orderedSavedDocuments.findIndex((doc) => doc.documentId === currentDocumentId) : -1; // Open documents in order around the current document, placing earlier ones before it and later ones after if (currentIndex !== -1 && currentDocumentId !== undefined) { for (let i = currentIndex - 1; i >= 0; i--) { const { documentId, document, details } = orderedSavedDocuments[i]; const { name, isSaved } = details; editor.openAutoSavedDocument(documentId, name, isSaved, document, true); } for (let i = currentIndex + 1; i < orderedSavedDocuments.length; i++) { const { documentId, document, details } = orderedSavedDocuments[i]; const { name, isSaved } = details; editor.openAutoSavedDocument(documentId, name, isSaved, document, false); } editor.selectDocument(currentDocumentId); } // No valid current document: open all remaining documents and select the last one else { const length = orderedSavedDocuments.length; for (let i = length - 2; i >= 0; i--) { const { documentId, document, details } = orderedSavedDocuments[i]; const { name, isSaved } = details; editor.openAutoSavedDocument(documentId, name, isSaved, document, true); } if (length > 0) editor.selectDocument(orderedSavedDocuments[length - 1].documentId); } } export async function saveActiveDocument(documentId: bigint) { const previouslySavedDocuments = await databaseGet>>("documents"); const documentIdString = String(documentId); // TODO: Eventually remove this document upgrade code // Migrate TriggerPersistenceWriteDocument.documentId from string to bigint if needed if (previouslySavedDocuments) { Object.values(previouslySavedDocuments).forEach((doc) => { if (typeof doc.documentId === "string") doc.documentId = BigInt(doc.documentId); }); } if (!previouslySavedDocuments) return; if (documentIdString in previouslySavedDocuments) { await storeCurrentDocumentId(documentIdString); } } export async function saveEditorPreferences(preferences: unknown) { await databaseSet("preferences", preferences); } export async function loadEditorPreferences(editor: EditorWrapper) { const preferences = await databaseGet>("preferences"); editor.loadPreferences(preferences ? JSON.stringify(preferences) : undefined); } export async function saveWorkspaceLayout(layout: unknown) { await databaseSet("workspace_layout", layout); } export async function loadWorkspaceLayout(editor: EditorWrapper) { const layout = await databaseGet>("workspace_layout"); if (layout) editor.loadWorkspaceLayout(layout); } export async function wipeDocuments() { await databaseDelete("documents_tab_order"); await databaseDelete("current_document_id"); await databaseDelete("documents"); } function databaseOpen(): Promise { return new Promise((resolve, reject) => { const request = indexedDB.open(PERSISTENCE_DB, 1); request.onupgradeneeded = () => { if (!request.result.objectStoreNames.contains(PERSISTENCE_STORE)) { request.result.createObjectStore(PERSISTENCE_STORE); } }; request.onsuccess = () => resolve(request.result); request.onerror = () => reject(request.error); }); } async function databaseGet(key: string): Promise { const db = await databaseOpen(); return new Promise((resolve, reject) => { const transaction = db.transaction(PERSISTENCE_STORE, "readonly"); const request = transaction.objectStore(PERSISTENCE_STORE).get(key); request.onsuccess = () => { const result: T | undefined = request.result; resolve(result); }; request.onerror = () => reject(request.error); }); } async function databaseSet(key: string, value: unknown): Promise { const db = await databaseOpen(); return new Promise((resolve, reject) => { const transaction = db.transaction(PERSISTENCE_STORE, "readwrite"); transaction.objectStore(PERSISTENCE_STORE).put(value, key); transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }); } async function databaseDelete(key: string): Promise { const db = await databaseOpen(); return new Promise((resolve, reject) => { const transaction = db.transaction(PERSISTENCE_STORE, "readwrite"); transaction.objectStore(PERSISTENCE_STORE).delete(key); transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }); } async function databaseUpdate(key: string, updater: (existing: T | undefined) => T): Promise { const db = await databaseOpen(); return new Promise((resolve, reject) => { const transaction = db.transaction(PERSISTENCE_STORE, "readwrite"); const store = transaction.objectStore(PERSISTENCE_STORE); const getRequest = store.get(key); getRequest.onsuccess = () => { const existing: T | undefined = getRequest.result; store.put(updater(existing), key); }; transaction.oncomplete = () => resolve(); transaction.onerror = () => reject(transaction.error); }); }