From 1867662653cfa22a52cbb7dcff05bbcd488b3d4d Mon Sep 17 00:00:00 2001 From: Youzini-afk <13153778771cx@gmail.com> Date: Tue, 14 Apr 2026 19:55:18 +0800 Subject: [PATCH] fix: align cleanup actions with opfs storage --- index.js | 8 +++ sync/bme-opfs-store.js | 123 ++++++++++++++++++++++++++++++++++++ tests/opfs-persistence.mjs | 56 ++++++++++++++++ ui/panel.html | 10 +-- ui/panel.js | 12 ++-- ui/ui-actions-controller.js | 61 ++++++++++++++---- 6 files changed, 246 insertions(+), 24 deletions(-) diff --git a/index.js b/index.js index 5d3dd58..657558c 100644 --- a/index.js +++ b/index.js @@ -31,6 +31,8 @@ import { BME_GRAPH_LOCAL_STORAGE_MODE_INDEXEDDB, BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_PRIMARY, BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_SHADOW, + deleteAllOpfsStorage, + deleteOpfsChatStorage, OpfsGraphStore, detectOpfsSupport, isGraphLocalStorageModeOpfs, @@ -15280,6 +15282,7 @@ const _cleanupRuntime = () => ({ buildBmeDbName, buildRestoreSafetyDbName: (chatId) => buildBmeDbName(buildRestoreSafetyChatId(chatId)), + buildRestoreSafetyChatId, closeBmeDb: async (chatId) => { const normalizedChatId = normalizeChatIdCandidate(chatId); if (!normalizedChatId || !bmeChatManager) return; @@ -15299,10 +15302,15 @@ const _cleanupRuntime = () => ({ clearCachedIndexedDbSnapshot, clearAllCachedIndexedDbSnapshots, clearCurrentChatCommitMarker, + deleteCurrentChatOpfsStorage: async (chatId) => + await deleteOpfsChatStorage(chatId), + deleteAllOpfsStorage: async () => + await deleteAllOpfsStorage(), deleteRemoteSyncFile: (chatId) => deleteRemoteSyncFile(chatId, { fetch: globalThis.fetch?.bind(globalThis), getRequestHeaders: typeof getRequestHeaders === "function" ? getRequestHeaders : undefined, }), + getGraphPersistenceState: () => graphPersistenceState, toastr, }); diff --git a/sync/bme-opfs-store.js b/sync/bme-opfs-store.js index de50d04..a527122 100644 --- a/sync/bme-opfs-store.js +++ b/sync/bme-opfs-store.js @@ -340,6 +340,19 @@ async function maybeGetFileHandle(parentHandle, name) { } } +async function maybeGetDirectoryHandle(parentHandle, name) { + try { + return await parentHandle.getDirectoryHandle(String(name || ""), { + create: false, + }); + } catch (error) { + if (isNotFoundError(error)) { + return null; + } + throw error; + } +} + async function readJsonFile(parentHandle, name, fallbackValue = null) { const fileHandle = await maybeGetFileHandle(parentHandle, name); if (!fileHandle) { @@ -599,6 +612,116 @@ export async function detectOpfsSupport(options = {}) { } } +export async function deleteOpfsChatStorage(chatId, options = {}) { + const normalizedChatId = normalizeChatId(chatId); + if (!normalizedChatId) { + return { + deleted: false, + reason: "missing-chat-id", + chatId: "", + }; + } + const rootDirectoryFactory = + typeof options.rootDirectoryFactory === "function" + ? options.rootDirectoryFactory + : getDefaultOpfsRootDirectory; + try { + const rootDirectory = await rootDirectoryFactory(); + if (!rootDirectory || typeof rootDirectory.getDirectoryHandle !== "function") { + return { + deleted: false, + reason: "missing-directory-handle", + chatId: normalizedChatId, + }; + } + const opfsRoot = await maybeGetDirectoryHandle( + rootDirectory, + OPFS_ROOT_DIRECTORY_NAME, + ); + if (!opfsRoot) { + return { + deleted: false, + reason: "not-found", + chatId: normalizedChatId, + }; + } + const chatsDirectory = await maybeGetDirectoryHandle( + opfsRoot, + OPFS_CHATS_DIRECTORY_NAME, + ); + if (!chatsDirectory) { + return { + deleted: false, + reason: "not-found", + chatId: normalizedChatId, + }; + } + const chatDirectoryName = buildChatDirectoryName(normalizedChatId); + const chatDirectory = await maybeGetDirectoryHandle(chatsDirectory, chatDirectoryName); + if (!chatDirectory) { + return { + deleted: false, + reason: "not-found", + chatId: normalizedChatId, + }; + } + await chatsDirectory.removeEntry(chatDirectoryName, { + recursive: true, + }); + return { + deleted: true, + reason: "deleted", + chatId: normalizedChatId, + }; + } catch (error) { + return { + deleted: false, + reason: "delete-failed", + chatId: normalizedChatId, + error, + }; + } +} + +export async function deleteAllOpfsStorage(options = {}) { + const rootDirectoryFactory = + typeof options.rootDirectoryFactory === "function" + ? options.rootDirectoryFactory + : getDefaultOpfsRootDirectory; + try { + const rootDirectory = await rootDirectoryFactory(); + if (!rootDirectory || typeof rootDirectory.getDirectoryHandle !== "function") { + return { + deleted: false, + reason: "missing-directory-handle", + }; + } + const opfsRoot = await maybeGetDirectoryHandle( + rootDirectory, + OPFS_ROOT_DIRECTORY_NAME, + ); + if (!opfsRoot) { + return { + deleted: false, + reason: "not-found", + }; + } + await rootDirectory.removeEntry(OPFS_ROOT_DIRECTORY_NAME, { + recursive: true, + }); + return { + deleted: true, + reason: "deleted", + }; + } catch (error) { + return { + deleted: false, + reason: "delete-failed", + error, + }; + } +} + class LegacyOpfsGraphStore { constructor(chatId, options = {}) { this.chatId = normalizeChatId(chatId); diff --git a/tests/opfs-persistence.mjs b/tests/opfs-persistence.mjs index 3be76b7..10f7095 100644 --- a/tests/opfs-persistence.mjs +++ b/tests/opfs-persistence.mjs @@ -8,6 +8,8 @@ import { import { BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_PRIMARY, BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_SHADOW, + deleteAllOpfsStorage, + deleteOpfsChatStorage, OpfsGraphStore, detectOpfsSupport, } from "../sync/bme-opfs-store.js"; @@ -520,6 +522,59 @@ async function testPruneExpiredTombstonesAndClearAll() { await store.close(); } +async function testDeleteCurrentAndAllOpfsStorage() { + const rootDirectory = createMemoryOpfsRoot(); + const storeA = new OpfsGraphStore("chat-opfs-delete-a", { + rootDirectoryFactory: async () => rootDirectory, + storeMode: BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_PRIMARY, + }); + const storeB = new OpfsGraphStore("chat-opfs-delete-b", { + rootDirectoryFactory: async () => rootDirectory, + storeMode: BME_GRAPH_LOCAL_STORAGE_MODE_OPFS_PRIMARY, + }); + await storeA.open(); + await storeB.open(); + + await storeA.importSnapshot( + { + meta: { revision: 1 }, + state: { lastProcessedFloor: 1, extractionCount: 1 }, + nodes: [{ id: "node-a", type: "event", fields: { title: "A" }, archived: false, updatedAt: 1 }], + edges: [], + tombstones: [], + }, + { mode: "replace", preserveRevision: true }, + ); + await storeB.importSnapshot( + { + meta: { revision: 1 }, + state: { lastProcessedFloor: 1, extractionCount: 1 }, + nodes: [{ id: "node-b", type: "event", fields: { title: "B" }, archived: false, updatedAt: 1 }], + edges: [], + tombstones: [], + }, + { mode: "replace", preserveRevision: true }, + ); + + const deleteCurrentResult = await deleteOpfsChatStorage("chat-opfs-delete-a", { + rootDirectoryFactory: async () => rootDirectory, + }); + assert.equal(deleteCurrentResult.deleted, true); + + const chatsDirectory = getNestedDirectory( + getNestedDirectory(rootDirectory, "st-bme"), + "chats", + ); + assert.equal(chatsDirectory.directories.has(encodeURIComponent("chat-opfs-delete-a")), false); + assert.equal(chatsDirectory.directories.has(encodeURIComponent("chat-opfs-delete-b")), true); + + const deleteAllResult = await deleteAllOpfsStorage({ + rootDirectoryFactory: async () => rootDirectory, + }); + assert.equal(deleteAllResult.deleted, true); + assert.equal(rootDirectory.directories.has("st-bme"), false); +} + async function main() { console.log(`${PREFIX} starting`); @@ -527,6 +582,7 @@ async function main() { await testImportExportPersistenceAndFileRotation(); await testImportLegacyGraphMigrationAndSkipPaths(); await testPruneExpiredTombstonesAndClearAll(); + await testDeleteCurrentAndAllOpfsStorage(); console.log("opfs-persistence tests passed"); } diff --git a/ui/panel.html b/ui/panel.html index 018a8e7..1f6ac72 100644 --- a/ui/panel.html +++ b/ui/panel.html @@ -2921,7 +2921,7 @@