From 2daada5089df7aa245fa40f76766bdbccdc91dc4 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Fri, 20 Mar 2026 14:43:24 +0800 Subject: [PATCH 1/2] fix(accounts): compact legacy stored account comments --- src/stores/accounts/accounts-database.test.ts | 62 +++++++++++++++++++ src/stores/accounts/accounts-database.ts | 34 +++++++++- 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/src/stores/accounts/accounts-database.test.ts b/src/stores/accounts/accounts-database.test.ts index a63fd9c..1647796 100644 --- a/src/stores/accounts/accounts-database.test.ts +++ b/src/stores/accounts/accounts-database.test.ts @@ -758,6 +758,37 @@ describe("accounts-database", () => { expect(parsed.accountComments).toHaveLength(1); expect(parsed.accountComments[0].cid).toBe("ec1"); }); + + test("compacts legacy stored accountComments before export", async () => { + const acc = makeAccount({ id: "export-legacy-comments", name: "ExportLegacyComments" }); + await accountsDatabase.addAccount(acc); + const commentsDb = createPerAccountDatabase("accountComments", acc.id); + await commentsDb.setItem("0", { + cid: "legacy-comment", + content: "legacy", + communityAddress: "sub", + timestamp: 1, + author: { address: "addr" }, + replies: { + pages: { + best: { + comments: [{ cid: "reply-1", content: "reply" }], + }, + }, + pageCids: { best: "page-1" }, + }, + }); + await commentsDb.setItem("length", 1); + + const exported = JSON.parse(await accountsDatabase.getExportedAccountJson(acc.id)); + const storedComment = await commentsDb.getItem("0"); + + expect(exported.accountComments[0].replies?.pages).toBeUndefined(); + expect(exported.accountComments[0].replies?.pageCids).toEqual({ best: "page-1" }); + expect(storedComment.replies?.pages).toBeUndefined(); + expect(storedComment.replies?.pageCids).toEqual({ best: "page-1" }); + expect(await commentsDb.getItem("__storageVersion")).toBe(1); + }); }); describe("account comments", () => { @@ -809,6 +840,37 @@ describe("accounts-database", () => { expect(exported.accountComments[0].replies?.pages).toBeUndefined(); }); + test("getAccountComments compacts legacy stored comments on read", async () => { + const acc = makeAccount({ id: "legacy-read-comments", name: "LegacyReadComments" }); + await accountsDatabase.addAccount(acc); + const commentsDb = createPerAccountDatabase("accountComments", acc.id); + await commentsDb.setItem("0", { + cid: "legacy-read-comment", + content: "legacy", + communityAddress: "sub", + timestamp: 1, + author: { address: "addr" }, + replies: { + pages: { + best: { + comments: [{ cid: "reply-1", content: "reply" }], + }, + }, + pageCids: { best: "page-1" }, + }, + }); + await commentsDb.setItem("length", 1); + + const comments = await accountsDatabase.getAccountComments(acc.id); + const storedComment = await commentsDb.getItem("0"); + + expect(comments[0].replies?.pages).toBeUndefined(); + expect(comments[0].replies?.pageCids).toEqual({ best: "page-1" }); + expect(storedComment.replies?.pages).toBeUndefined(); + expect(storedComment.replies?.pageCids).toEqual({ best: "page-1" }); + expect(await commentsDb.getItem("__storageVersion")).toBe(1); + }); + test("addAccountComment asserts accountCommentIndex < length", async () => { const acc = makeAccount({ id: "edit-assert", name: "EditAssert" }); await accountsDatabase.addAccount(acc); diff --git a/src/stores/accounts/accounts-database.ts b/src/stores/accounts/accounts-database.ts index a8fcaaf..d632853 100644 --- a/src/stores/accounts/accounts-database.ts +++ b/src/stores/accounts/accounts-database.ts @@ -36,6 +36,7 @@ const storageVersionKey = "__storageVersion"; const votesLatestIndexKey = "__commentCidToLatestIndex"; const editsTargetToIndicesKey = "__targetToIndices"; const editsSummaryKey = "__summary"; +const commentStorageVersion = 1; const voteStorageVersion = 1; const editStorageVersion = 1; @@ -187,6 +188,7 @@ const getExportedAccountJson = async (accountId: string) => { const accountCommentsDatabase = getAccountCommentsDatabase(accountId); const accountVotesDatabase = getAccountVotesDatabase(accountId); const accountEditsDatabase = getAccountEditsDatabase(accountId); + await ensureAccountCommentsDatabaseLayout(accountId); const [accountComments, accountVotes, accountEdits] = await Promise.all([ getDatabaseAsArray(accountCommentsDatabase), getDatabaseAsArray(accountVotesDatabase), @@ -361,8 +363,32 @@ const getAccountCommentsDatabase = (accountId: string) => { return accountsCommentsDatabases[accountId]; }; +const ensureAccountCommentsDatabaseLayout = async (accountId: string) => { + const accountCommentsDatabase = getAccountCommentsDatabase(accountId); + if ((await accountCommentsDatabase.getItem(storageVersionKey)) === commentStorageVersion) { + return; + } + + const comments = await getDatabaseAsArray(accountCommentsDatabase); + const updatedComments = comments.map((comment) => + comment ? sanitizeStoredAccountComment(comment) : comment, + ); + const rewritePromises = updatedComments + .map((comment, index) => + isEqual(comment, comments[index]) + ? null + : accountCommentsDatabase.setItem(String(index), comment), + ) + .filter(Boolean); + await Promise.all([ + ...rewritePromises, + accountCommentsDatabase.setItem(storageVersionKey, commentStorageVersion), + ]); +}; + const deleteAccountComment = async (accountId: string, accountCommentIndex: number) => { const accountCommentsDatabase = getAccountCommentsDatabase(accountId); + await ensureAccountCommentsDatabaseLayout(accountId); const length = (await accountCommentsDatabase.getItem("length")) || 0; assert( accountCommentIndex >= 0 && accountCommentIndex < length, @@ -386,6 +412,7 @@ const addAccountComment = async ( accountCommentIndex?: number, ) => { const accountCommentsDatabase = getAccountCommentsDatabase(accountId); + await ensureAccountCommentsDatabaseLayout(accountId); const length = (await accountCommentsDatabase.getItem("length")) || 0; comment = sanitizeStoredAccountComment(comment); if (typeof accountCommentIndex === "number") { @@ -393,10 +420,14 @@ const addAccountComment = async ( accountCommentIndex < length, `addAccountComment cannot edit comment no comment in database at accountCommentIndex '${accountCommentIndex}'`, ); - await accountCommentsDatabase.setItem(String(accountCommentIndex), comment); + await Promise.all([ + accountCommentsDatabase.setItem(String(accountCommentIndex), comment), + accountCommentsDatabase.setItem(storageVersionKey, commentStorageVersion), + ]); } else { await Promise.all([ accountCommentsDatabase.setItem(String(length), comment), + accountCommentsDatabase.setItem(storageVersionKey, commentStorageVersion), accountCommentsDatabase.setItem("length", length + 1), ]); } @@ -404,6 +435,7 @@ const addAccountComment = async ( const getAccountComments = async (accountId: string) => { const accountCommentsDatabase = getAccountCommentsDatabase(accountId); + await ensureAccountCommentsDatabaseLayout(accountId); const length = (await accountCommentsDatabase.getItem("length")) || 0; if (length === 0) { return []; From 901887f80809dd3bae7f93e86faec72b427a7e49 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Fri, 20 Mar 2026 14:59:19 +0800 Subject: [PATCH 2/2] fix(accounts): serialize legacy comment compaction --- src/stores/accounts/accounts-database.test.ts | 49 +++++++++++++++++++ src/stores/accounts/accounts-database.ts | 39 +++++++++------ 2 files changed, 73 insertions(+), 15 deletions(-) diff --git a/src/stores/accounts/accounts-database.test.ts b/src/stores/accounts/accounts-database.test.ts index 1647796..39a518c 100644 --- a/src/stores/accounts/accounts-database.test.ts +++ b/src/stores/accounts/accounts-database.test.ts @@ -871,6 +871,55 @@ describe("accounts-database", () => { expect(await commentsDb.getItem("__storageVersion")).toBe(1); }); + test("deleteAccountComment compacts legacy stored comments before mutating", async () => { + const acc = makeAccount({ id: "legacy-delete-comments", name: "LegacyDeleteComments" }); + await accountsDatabase.addAccount(acc); + const commentsDb = createPerAccountDatabase("accountComments", acc.id); + await commentsDb.setItem("0", { + cid: "legacy-delete-comment-1", + content: "legacy-1", + communityAddress: "sub", + timestamp: 1, + author: { address: "addr" }, + replies: { + pages: { + best: { + comments: [{ cid: "reply-1", content: "reply" }], + }, + }, + pageCids: { best: "page-1" }, + }, + }); + await commentsDb.setItem("1", { + cid: "legacy-delete-comment-2", + content: "legacy-2", + communityAddress: "sub", + timestamp: 2, + author: { address: "addr" }, + replies: { + pages: { + best: { + comments: [{ cid: "reply-2", content: "reply" }], + }, + }, + pageCids: { best: "page-2" }, + }, + }); + await commentsDb.setItem("length", 2); + + await accountsDatabase.deleteAccountComment(acc.id, 0); + const storedComment = await commentsDb.getItem("0"); + const exported = JSON.parse(await accountsDatabase.getExportedAccountJson(acc.id)); + + expect(storedComment.cid).toBe("legacy-delete-comment-2"); + expect(storedComment.replies?.pages).toBeUndefined(); + expect(storedComment.replies?.pageCids).toEqual({ best: "page-2" }); + expect(exported.accountComments).toHaveLength(1); + expect(exported.accountComments[0].replies?.pages).toBeUndefined(); + expect(exported.accountComments[0].replies?.pageCids).toEqual({ best: "page-2" }); + expect(await commentsDb.getItem("__storageVersion")).toBe(1); + }); + test("addAccountComment asserts accountCommentIndex < length", async () => { const acc = makeAccount({ id: "edit-assert", name: "EditAssert" }); await accountsDatabase.addAccount(acc); diff --git a/src/stores/accounts/accounts-database.ts b/src/stores/accounts/accounts-database.ts index d632853..fc97400 100644 --- a/src/stores/accounts/accounts-database.ts +++ b/src/stores/accounts/accounts-database.ts @@ -350,6 +350,7 @@ const removeAccount = async (account: Account) => { }; const accountsCommentsDatabases: any = {}; +const accountCommentsLayoutMigrations: Record | undefined> = {}; const getAccountCommentsDatabase = (accountId: string) => { assert( accountId && typeof accountId === "string", @@ -368,22 +369,30 @@ const ensureAccountCommentsDatabaseLayout = async (accountId: string) => { if ((await accountCommentsDatabase.getItem(storageVersionKey)) === commentStorageVersion) { return; } + if (!accountCommentsLayoutMigrations[accountId]) { + accountCommentsLayoutMigrations[accountId] = (async () => { + if ((await accountCommentsDatabase.getItem(storageVersionKey)) === commentStorageVersion) { + return; + } - const comments = await getDatabaseAsArray(accountCommentsDatabase); - const updatedComments = comments.map((comment) => - comment ? sanitizeStoredAccountComment(comment) : comment, - ); - const rewritePromises = updatedComments - .map((comment, index) => - isEqual(comment, comments[index]) - ? null - : accountCommentsDatabase.setItem(String(index), comment), - ) - .filter(Boolean); - await Promise.all([ - ...rewritePromises, - accountCommentsDatabase.setItem(storageVersionKey, commentStorageVersion), - ]); + const comments = await getDatabaseAsArray(accountCommentsDatabase); + const updatedComments = comments.map((comment) => + comment ? sanitizeStoredAccountComment(comment) : comment, + ); + const rewritePromises: Promise[] = []; + for (const [index, updatedComment] of updatedComments.entries()) { + if (!isEqual(updatedComment, comments[index])) { + rewritePromises.push(accountCommentsDatabase.setItem(String(index), updatedComment)); + } + } + await Promise.all(rewritePromises); + await accountCommentsDatabase.setItem(storageVersionKey, commentStorageVersion); + })().finally(() => { + delete accountCommentsLayoutMigrations[accountId]; + }); + } + + await accountCommentsLayoutMigrations[accountId]; }; const deleteAccountComment = async (accountId: string, accountCommentIndex: number) => {