diff --git a/src/stores/accounts/accounts-database.test.ts b/src/stores/accounts/accounts-database.test.ts index a63fd9c..39a518c 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,86 @@ 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("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 a8fcaaf..fc97400 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), @@ -348,6 +350,7 @@ const removeAccount = async (account: Account) => { }; const accountsCommentsDatabases: any = {}; +const accountCommentsLayoutMigrations: Record | undefined> = {}; const getAccountCommentsDatabase = (accountId: string) => { assert( accountId && typeof accountId === "string", @@ -361,8 +364,40 @@ const getAccountCommentsDatabase = (accountId: string) => { return accountsCommentsDatabases[accountId]; }; +const ensureAccountCommentsDatabaseLayout = async (accountId: string) => { + const accountCommentsDatabase = getAccountCommentsDatabase(accountId); + 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: 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) => { const accountCommentsDatabase = getAccountCommentsDatabase(accountId); + await ensureAccountCommentsDatabaseLayout(accountId); const length = (await accountCommentsDatabase.getItem("length")) || 0; assert( accountCommentIndex >= 0 && accountCommentIndex < length, @@ -386,6 +421,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 +429,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 +444,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 [];