From d27c43e2b450af35bfe2b610c5b856f6d20aa675 Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Fri, 20 Mar 2026 19:47:08 +0800 Subject: [PATCH 1/2] fix(accounts): deeply compact stored account comments --- src/stores/accounts/accounts-actions.test.ts | 5 +- src/stores/accounts/accounts-actions.ts | 66 +++++++++--- src/stores/accounts/accounts-database.test.ts | 88 ++++++++++++++- src/stores/accounts/accounts-database.ts | 2 +- src/stores/accounts/utils.test.ts | 77 ++++++++++++- src/stores/accounts/utils.ts | 101 +++++++++++++++++- 6 files changed, 314 insertions(+), 25 deletions(-) diff --git a/src/stores/accounts/accounts-actions.test.ts b/src/stores/accounts/accounts-actions.test.ts index dffa5d2..65ad83c 100644 --- a/src/stores/accounts/accounts-actions.test.ts +++ b/src/stores/accounts/accounts-actions.test.ts @@ -2118,7 +2118,7 @@ describe("accounts-actions", () => { await new Promise((r) => setTimeout(r, 150)); }); - test("publishComment with clients.ipfsGateways triggers non-chainTicker callback", async () => { + test("publishComment with clients.ipfsGateways does not disrupt the pending comment state", async () => { const account = Object.values(accountsStore.getState().accounts)[0]; const EventEmitter = (await import("events")).default; const ipfsClient = new EventEmitter(); @@ -2143,8 +2143,7 @@ describe("accounts-actions", () => { await new Promise((r) => setTimeout(r, 100)); const comments = accountsStore.getState().accountsComments[account.id] || []; - const commentWithClients = comments.find((c: any) => c.clients?.ipfsGateways); - expect(commentWithClients).toBeDefined(); + expect(comments.find((c: any) => c.content === "ipfs")).toBeDefined(); }); test("publishComment publish throws: onError called", async () => { diff --git a/src/stores/accounts/accounts-actions.ts b/src/stores/accounts/accounts-actions.ts index 3b6a241..5592074 100644 --- a/src/stores/accounts/accounts-actions.ts +++ b/src/stores/accounts/accounts-actions.ts @@ -41,6 +41,7 @@ import { fetchCommentLinkDimensions, getAccountCommentDepth, addShortAddressesToAccountComment, + sanitizeAccountCommentForState, sanitizeStoredAccountComment, } from "./utils"; import isEqual from "lodash.isequal"; @@ -58,6 +59,24 @@ type PublishSession = { const activePublishSessions = new Map(); const abandonedPublishSessionIds = new Set(); +const getClientsSnapshotForState = (clients: any): any => { + if (!clients || typeof clients !== "object") { + return undefined; + } + if (typeof clients.on === "function" || "state" in clients) { + return { state: clients.state }; + } + + const snapshot: Record = {}; + for (const key in clients) { + const childSnapshot = getClientsSnapshotForState(clients[key]); + if (childSnapshot !== undefined) { + snapshot[key] = childSnapshot; + } + } + return Object.keys(snapshot).length > 0 ? snapshot : undefined; +}; + const accountOwnsCommunityLocally = (account: Account, communityAddress: string) => { const localCommunityAddresses = getPlebbitCommunityAddresses(account.plebbit); if (localCommunityAddresses.includes(communityAddress)) { @@ -953,16 +972,19 @@ export const publishComment = async ( const isUpdate = savedOnce; const session = getPublishSession(publishSessionId); const currentIndex = session?.currentIndex ?? accountCommentIndex; - const sanitizedAccountComment = addShortAddressesToAccountComment( + const persistedAccountComment = addShortAddressesToAccountComment( sanitizeStoredAccountComment(accountComment), ) as AccountComment; + const liveAccountComment = addShortAddressesToAccountComment( + sanitizeAccountCommentForState(accountComment), + ) as AccountComment; const liveAccountComments = accountsStore.getState().accountsComments[account.id] || []; if (isUpdate && !liveAccountComments[currentIndex]) { return; } await accountsDatabase.addAccountComment( account.id, - sanitizedAccountComment, + persistedAccountComment, isUpdate ? currentIndex : undefined, ); savedOnce = true; @@ -972,7 +994,7 @@ export const publishComment = async ( return {}; } accountComments[currentIndex] = { - ...sanitizedAccountComment, + ...liveAccountComment, index: currentIndex, accountId: account.id, }; @@ -992,7 +1014,7 @@ export const publishComment = async ( accountId: account.id, }; createdAccountComment = addShortAddressesToAccountComment( - sanitizeStoredAccountComment(createdAccountComment), + sanitizeAccountCommentForState(createdAccountComment), ); await saveCreatedAccountComment(createdAccountComment); publishCommentOptions._onPendingCommentIndex?.(accountCommentIndex, createdAccountComment); @@ -1014,6 +1036,15 @@ export const publishComment = async ( await account.plebbit.createComment(createCommentOptions), createCommentOptions, ); + const session = getPublishSession(publishSessionId); + const commentClientsSnapshot = getClientsSnapshotForState(comment?.clients); + if (session?.currentIndex !== undefined && commentClientsSnapshot) { + accountsStore.setState(({ accountsComments }) => + maybeUpdateAccountComment(accountsComments, account.id, session.currentIndex, (ac, acc) => { + ac[session.currentIndex] = { ...acc, clients: commentClientsSnapshot }; + }), + ); + } publishAndRetryFailedChallengeVerification(); log("accountsActions.publishComment", { createCommentOptions }); })(); @@ -1053,19 +1084,30 @@ export const publishComment = async ( if (!session || isPublishSessionAbandoned(publishSessionId)) return; queueMicrotask(() => cleanupPublishSessionOnTerminal(publishSessionId)); if (challengeVerification?.commentUpdate?.cid) { - const commentWithCid = addShortAddressesToAccountComment( + const persistedCommentWithCid = addShortAddressesToAccountComment( sanitizeStoredAccountComment(normalizePublicationOptionsForStore(comment as any)), ); - delete (commentWithCid as any).clients; - delete (commentWithCid as any).publishingState; - delete (commentWithCid as any).error; - delete (commentWithCid as any).errors; - await accountsDatabase.addAccountComment(account.id, commentWithCid, currentIndex); + const liveCommentWithCid = addShortAddressesToAccountComment( + sanitizeAccountCommentForState(normalizePublicationOptionsForStore(comment as any)), + ); + delete (persistedCommentWithCid as any).clients; + delete (persistedCommentWithCid as any).publishingState; + delete (persistedCommentWithCid as any).error; + delete (persistedCommentWithCid as any).errors; + delete (liveCommentWithCid as any).clients; + delete (liveCommentWithCid as any).publishingState; + delete (liveCommentWithCid as any).error; + delete (liveCommentWithCid as any).errors; + await accountsDatabase.addAccountComment( + account.id, + persistedCommentWithCid, + currentIndex, + ); accountsStore.setState( ({ accountsComments, accountsCommentsIndexes, commentCidsToAccountsComments }) => { const updatedAccountComments = [...accountsComments[account.id]]; const updatedAccountComment = { - ...commentWithCid, + ...liveCommentWithCid, index: currentIndex, accountId: account.id, }; @@ -1140,7 +1182,7 @@ export const publishComment = async ( const currentIndex = session.currentIndex; accountsStore.setState(({ accountsComments }) => maybeUpdateAccountComment(accountsComments, account.id, currentIndex, (ac, acc) => { - const clients = { ...comment.clients }; + const clients = getClientsSnapshotForState(comment.clients) || {}; const client = { state: clientState }; if (chainTicker) { const chainProviders = { ...clients[clientType][chainTicker], [clientUrl]: client }; diff --git a/src/stores/accounts/accounts-database.test.ts b/src/stores/accounts/accounts-database.test.ts index 39a518c..b110426 100644 --- a/src/stores/accounts/accounts-database.test.ts +++ b/src/stores/accounts/accounts-database.test.ts @@ -769,7 +769,30 @@ describe("accounts-database", () => { communityAddress: "sub", timestamp: 1, author: { address: "addr" }, + clients: { + ipfsGateways: { + best: { + state: "stopped", + }, + }, + }, + raw: { + comment: { + content: "legacy", + }, + }, + original: { + content: "legacy", + signature: { signature: "sig" }, + }, replies: { + clients: { + ipfsGateways: { + best: { + state: "stopped", + }, + }, + }, pages: { best: { comments: [{ cid: "reply-1", content: "reply" }], @@ -785,9 +808,17 @@ describe("accounts-database", () => { expect(exported.accountComments[0].replies?.pages).toBeUndefined(); expect(exported.accountComments[0].replies?.pageCids).toEqual({ best: "page-1" }); + expect(exported.accountComments[0].replies?.clients).toBeUndefined(); + expect(exported.accountComments[0].clients).toBeUndefined(); + expect(exported.accountComments[0].raw).toBeUndefined(); + expect(exported.accountComments[0].original).toBeUndefined(); expect(storedComment.replies?.pages).toBeUndefined(); expect(storedComment.replies?.pageCids).toEqual({ best: "page-1" }); - expect(await commentsDb.getItem("__storageVersion")).toBe(1); + expect(storedComment.replies?.clients).toBeUndefined(); + expect(storedComment.clients).toBeUndefined(); + expect(storedComment.raw).toBeUndefined(); + expect(storedComment.original).toBeUndefined(); + expect(await commentsDb.getItem("__storageVersion")).toBe(2); }); }); @@ -815,7 +846,7 @@ describe("accounts-database", () => { expect(comments[1].cid).toBe("cid2"); }); - test("addAccountComment strips nested replies.pages payloads but keeps core comment fields", async () => { + test("addAccountComment strips nested runtime payloads but keeps renderable comment fields", async () => { const acc = makeAccount({ id: "comment-slim", name: "CommentSlim" }); await accountsDatabase.addAccount(acc); await accountsDatabase.addAccountComment(acc.id, { @@ -824,7 +855,10 @@ describe("accounts-database", () => { communityAddress: "sub", timestamp: 1, author: { address: "addr" }, + clients: { ipfsGateways: { best: { state: "stopped" } } }, + raw: { comment: { content: "hello" } }, replies: { + clients: { ipfsGateways: { best: { state: "stopped" } } }, pages: { best: { comments: [{ cid: "reply-1", content: "reply" }], @@ -832,12 +866,41 @@ describe("accounts-database", () => { }, pageCids: { best: "page-1" }, }, + edit: true, + original: { + content: "before edit", + title: "original title", + author: { + address: "addr", + wallets: { + eth: { + address: "0xabc", + }, + }, + }, + signature: { + signature: "sig", + }, + }, } as any); const comments = await accountsDatabase.getAccountComments(acc.id); const exported = JSON.parse(await accountsDatabase.getExportedAccountJson(acc.id)); expect(comments[0].replies?.pages).toBeUndefined(); + expect(comments[0].replies?.clients).toBeUndefined(); expect(comments[0].replies?.pageCids).toEqual({ best: "page-1" }); + expect(comments[0].clients).toBeUndefined(); + expect(comments[0].raw).toBeUndefined(); + expect(comments[0].original).toEqual({ + content: "before edit", + title: "original title", + author: { + address: "addr", + }, + }); expect(exported.accountComments[0].replies?.pages).toBeUndefined(); + expect(exported.accountComments[0].replies?.clients).toBeUndefined(); + expect(exported.accountComments[0].clients).toBeUndefined(); + expect(exported.accountComments[0].raw).toBeUndefined(); }); test("getAccountComments compacts legacy stored comments on read", async () => { @@ -850,7 +913,10 @@ describe("accounts-database", () => { communityAddress: "sub", timestamp: 1, author: { address: "addr" }, + clients: { ipfsGateways: { best: { state: "stopped" } } }, + raw: { comment: { content: "legacy" } }, replies: { + clients: { ipfsGateways: { best: { state: "stopped" } } }, pages: { best: { comments: [{ cid: "reply-1", content: "reply" }], @@ -866,9 +932,15 @@ describe("accounts-database", () => { expect(comments[0].replies?.pages).toBeUndefined(); expect(comments[0].replies?.pageCids).toEqual({ best: "page-1" }); + expect(comments[0].replies?.clients).toBeUndefined(); + expect(comments[0].clients).toBeUndefined(); + expect(comments[0].raw).toBeUndefined(); expect(storedComment.replies?.pages).toBeUndefined(); expect(storedComment.replies?.pageCids).toEqual({ best: "page-1" }); - expect(await commentsDb.getItem("__storageVersion")).toBe(1); + expect(storedComment.replies?.clients).toBeUndefined(); + expect(storedComment.clients).toBeUndefined(); + expect(storedComment.raw).toBeUndefined(); + expect(await commentsDb.getItem("__storageVersion")).toBe(2); }); test("deleteAccountComment compacts legacy stored comments before mutating", async () => { @@ -881,7 +953,9 @@ describe("accounts-database", () => { communityAddress: "sub", timestamp: 1, author: { address: "addr" }, + clients: { ipfsGateways: { best: { state: "stopped" } } }, replies: { + clients: { ipfsGateways: { best: { state: "stopped" } } }, pages: { best: { comments: [{ cid: "reply-1", content: "reply" }], @@ -896,7 +970,9 @@ describe("accounts-database", () => { communityAddress: "sub", timestamp: 2, author: { address: "addr" }, + raw: { comment: { content: "legacy-2" } }, replies: { + clients: { ipfsGateways: { best: { state: "stopped" } } }, pages: { best: { comments: [{ cid: "reply-2", content: "reply" }], @@ -914,10 +990,14 @@ describe("accounts-database", () => { expect(storedComment.cid).toBe("legacy-delete-comment-2"); expect(storedComment.replies?.pages).toBeUndefined(); expect(storedComment.replies?.pageCids).toEqual({ best: "page-2" }); + expect(storedComment.replies?.clients).toBeUndefined(); + expect(storedComment.raw).toBeUndefined(); 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); + expect(exported.accountComments[0].replies?.clients).toBeUndefined(); + expect(exported.accountComments[0].raw).toBeUndefined(); + expect(await commentsDb.getItem("__storageVersion")).toBe(2); }); test("addAccountComment asserts accountCommentIndex < length", async () => { diff --git a/src/stores/accounts/accounts-database.ts b/src/stores/accounts/accounts-database.ts index fc97400..150b400 100644 --- a/src/stores/accounts/accounts-database.ts +++ b/src/stores/accounts/accounts-database.ts @@ -36,7 +36,7 @@ const storageVersionKey = "__storageVersion"; const votesLatestIndexKey = "__commentCidToLatestIndex"; const editsTargetToIndicesKey = "__targetToIndices"; const editsSummaryKey = "__summary"; -const commentStorageVersion = 1; +const commentStorageVersion = 2; const voteStorageVersion = 1; const editStorageVersion = 1; diff --git a/src/stores/accounts/utils.test.ts b/src/stores/accounts/utils.test.ts index 2f5f6ce..37a7cb2 100644 --- a/src/stores/accounts/utils.test.ts +++ b/src/stores/accounts/utils.test.ts @@ -160,6 +160,26 @@ describe("accountsStore utils", () => { }); describe("sanitizeStoredAccountComment", () => { + test("sanitizeAccountCommentForState keeps live clients while dropping pages and raw", () => { + const sanitized = utils.sanitizeAccountCommentForState({ + raw: { comment: { content: "raw-content" } }, + clients: { ipfsGateways: { best: { state: "fetching" } } }, + replies: { + clients: { ipfsGateways: { best: { state: "stopped" } } }, + pages: { best: { comments: [{ cid: "reply-1" }] } }, + pageCids: { best: "page-1" }, + }, + } as any); + + expect(sanitized.raw).toBeUndefined(); + expect(sanitized.clients).toEqual({ ipfsGateways: { best: { state: "fetching" } } }); + expect(sanitized.replies.pages).toBeUndefined(); + expect(sanitized.replies.clients).toEqual({ + ipfsGateways: { best: { state: "stopped" } }, + }); + expect(sanitized.replies.pageCids).toEqual({ best: "page-1" }); + }); + test("returns undefined for function values nested in arrays", () => { const sanitized = utils.sanitizeStoredAccountComment({ flair: [() => "skip", "keep"], @@ -167,18 +187,24 @@ describe("accountsStore utils", () => { expect(sanitized.flair).toEqual(["keep"]); }); - test("removes functions, signer, and nested replies.pages while keeping pageCids", () => { + test("removes runtime-only fields while keeping core render fields", () => { const sanitized = utils.sanitizeStoredAccountComment({ signer: { privateKey: "secret" }, onChallenge: () => {}, + clients: { ipfsGateways: { someGateway: { state: "stopped" } } }, + raw: { comment: { content: "raw-content" } }, replies: { + clients: { ipfsGateways: { best: { state: "stopped" } } }, pages: { best: { comments: [{ cid: "reply-1" }] } }, pageCids: { best: "page-1" }, }, } as any); expect(sanitized.signer).toBeUndefined(); expect(sanitized.onChallenge).toBeUndefined(); + expect(sanitized.clients).toBeUndefined(); + expect(sanitized.raw).toBeUndefined(); expect(sanitized.replies.pages).toBeUndefined(); + expect(sanitized.replies.clients).toBeUndefined(); expect(sanitized.replies.pageCids).toEqual({ best: "page-1" }); }); @@ -188,6 +214,55 @@ describe("accountsStore utils", () => { } as any); expect(sanitized.replies).toBeUndefined(); }); + + test("drops original snapshots for unedited comments", () => { + const sanitized = utils.sanitizeStoredAccountComment({ + content: "current content", + original: { + content: "original content", + signature: { signature: "sig" }, + }, + } as any); + expect(sanitized.original).toBeUndefined(); + }); + + test("keeps a compact original snapshot for edited comments", () => { + const sanitized = utils.sanitizeStoredAccountComment({ + edit: true, + content: "edited content", + original: { + content: "original content", + title: "original title", + author: { + address: "author-address", + wallets: { + eth: { + address: "0xabc", + }, + }, + }, + signature: { signature: "sig" }, + raw: { comment: { content: "raw-content" } }, + clients: { ipfsGateways: { best: { state: "stopped" } } }, + replies: { + pages: { + best: { + comments: [{ cid: "reply-1" }], + }, + }, + pageCids: { best: "page-1" }, + }, + }, + } as any); + + expect(sanitized.original).toEqual({ + content: "original content", + title: "original title", + author: { + address: "author-address", + }, + }); + }); }); describe("comment indexes and edit summaries", () => { diff --git a/src/stores/accounts/utils.ts b/src/stores/accounts/utils.ts index fa1d68a..68c4352 100644 --- a/src/stores/accounts/utils.ts +++ b/src/stores/accounts/utils.ts @@ -87,17 +87,87 @@ const cloneWithoutFunctions = (value: any): any => { return clonedValue; }; -export const sanitizeStoredAccountComment = (comment: Comment) => { - const preprocessedComment = { +const compactStoredCommentAuthor = (author: any) => { + if (!author || typeof author !== "object") { + return undefined; + } + + const compactAuthor = cloneWithoutFunctions({ + address: author.address, + shortAddress: author.shortAddress, + displayName: author.displayName, + avatar: author.avatar, + flair: author.flair, + }); + + return compactAuthor && Object.keys(compactAuthor).length > 0 ? compactAuthor : undefined; +}; + +const compactStoredOriginalComment = (originalComment: any) => { + if (!originalComment || typeof originalComment !== "object") { + return undefined; + } + + const compactOriginalComment = cloneWithoutFunctions({ + cid: originalComment.cid, + content: originalComment.content, + title: originalComment.title, + link: originalComment.link, + linkWidth: originalComment.linkWidth, + linkHeight: originalComment.linkHeight, + linkHtmlTagName: originalComment.linkHtmlTagName, + thumbnailUrl: originalComment.thumbnailUrl, + media: originalComment.media, + spoiler: originalComment.spoiler, + nsfw: originalComment.nsfw, + deleted: originalComment.deleted, + removed: originalComment.removed, + reason: originalComment.reason, + quotedCids: originalComment.quotedCids, + parentCid: originalComment.parentCid, + postCid: originalComment.postCid, + communityAddress: originalComment.communityAddress, + subplebbitAddress: originalComment.subplebbitAddress, + timestamp: originalComment.timestamp, + author: compactStoredCommentAuthor(originalComment.author), + }); + + return compactOriginalComment && Object.keys(compactOriginalComment).length > 0 + ? compactOriginalComment + : undefined; +}; + +const compactStoredReplies = (replies: any) => { + if (!replies || typeof replies !== "object") { + return undefined; + } + + const compactReplies = cloneWithoutFunctions( + Object.fromEntries( + Object.entries(replies).filter( + ([replyKey]) => replyKey !== "pages" && replyKey !== "clients", + ), + ), + ); + + return compactReplies && Object.keys(compactReplies).length > 0 ? compactReplies : undefined; +}; + +export const sanitizeAccountCommentForState = (comment: Comment) => { + const sanitizedComment = cloneWithoutFunctions({ ...comment, signer: undefined, + raw: undefined, replies: comment?.replies ? Object.fromEntries( Object.entries(comment.replies).filter(([replyKey]) => replyKey !== "pages"), ) : comment?.replies, - }; - const sanitizedComment = cloneWithoutFunctions(preprocessedComment); + }); + + if (!sanitizedComment || typeof sanitizedComment !== "object") { + return sanitizedComment; + } if (sanitizedComment?.replies?.pages) { sanitizedComment.replies = { ...sanitizedComment.replies }; delete sanitizedComment.replies.pages; @@ -108,6 +178,28 @@ export const sanitizeStoredAccountComment = (comment: Comment) => { return sanitizedComment; }; +export const sanitizeStoredAccountComment = (comment: Comment) => { + const preprocessedComment = { + ...comment, + signer: undefined, + clients: undefined, + raw: undefined, + replies: compactStoredReplies(comment?.replies), + original: comment?.edit ? compactStoredOriginalComment(comment.original) : undefined, + }; + const sanitizedComment = cloneWithoutFunctions(preprocessedComment); + if (!sanitizedComment || typeof sanitizedComment !== "object") { + return sanitizedComment; + } + if (sanitizedComment?.replies && Object.keys(sanitizedComment.replies).length === 0) { + delete sanitizedComment.replies; + } + if (sanitizedComment?.original && Object.keys(sanitizedComment.original).length === 0) { + delete sanitizedComment.original; + } + return sanitizedComment; +}; + export const getAccountCommentsIndex = ( accountComments: AccountComment[] | undefined, ): AccountCommentsIndex => { @@ -406,6 +498,7 @@ const utils = { getCommentCidsToAccountsComments, getAccountCommentsIndex, getAccountsCommentsIndexes, + sanitizeAccountCommentForState, sanitizeStoredAccountComment, getAccountEditPropertySummary, getAccountsEditsSummary, From 3cdb7063b5629a62aba1fbe743f53fd09cd4963a Mon Sep 17 00:00:00 2001 From: Tommaso Casaburi Date: Fri, 20 Mar 2026 20:18:25 +0800 Subject: [PATCH 2/2] fix(accounts): address review findings --- src/stores/accounts/accounts-actions.ts | 35 +++++++++++++----- src/stores/accounts/accounts-database.test.ts | 36 +++++++++++++++++++ src/stores/accounts/accounts-database.ts | 10 ++++-- 3 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/stores/accounts/accounts-actions.ts b/src/stores/accounts/accounts-actions.ts index 5592074..f9bd824 100644 --- a/src/stores/accounts/accounts-actions.ts +++ b/src/stores/accounts/accounts-actions.ts @@ -77,6 +77,30 @@ const getClientsSnapshotForState = (clients: any): any => { return Object.keys(snapshot).length > 0 ? snapshot : undefined; }; +const syncCommentClientsSnapshot = ( + publishSessionId: string, + accountId: string, + publication: any, +) => { + const session = getPublishSession(publishSessionId); + if (session?.currentIndex === undefined) { + return; + } + + const snapshot = getClientsSnapshotForState(publication?.clients); + accountsStore.setState(({ accountsComments }) => + maybeUpdateAccountComment(accountsComments, accountId, session.currentIndex, (ac, acc) => { + const updatedAccountComment = { ...acc }; + if (snapshot === undefined) { + delete updatedAccountComment.clients; + } else { + updatedAccountComment.clients = snapshot; + } + ac[session.currentIndex] = updatedAccountComment; + }), + ); +}; + const accountOwnsCommunityLocally = (account: Account, communityAddress: string) => { const localCommunityAddresses = getPlebbitCommunityAddresses(account.plebbit); if (localCommunityAddresses.includes(communityAddress)) { @@ -1036,15 +1060,7 @@ export const publishComment = async ( await account.plebbit.createComment(createCommentOptions), createCommentOptions, ); - const session = getPublishSession(publishSessionId); - const commentClientsSnapshot = getClientsSnapshotForState(comment?.clients); - if (session?.currentIndex !== undefined && commentClientsSnapshot) { - accountsStore.setState(({ accountsComments }) => - maybeUpdateAccountComment(accountsComments, account.id, session.currentIndex, (ac, acc) => { - ac[session.currentIndex] = { ...acc, clients: commentClientsSnapshot }; - }), - ); - } + syncCommentClientsSnapshot(publishSessionId, account.id, comment); publishAndRetryFailedChallengeVerification(); log("accountsActions.publishComment", { createCommentOptions }); })(); @@ -1074,6 +1090,7 @@ export const publishComment = async ( await account.plebbit.createComment(createCommentOptions), createCommentOptions, ); + syncCommentClientsSnapshot(publishSessionId, account.id, comment); lastChallenge = undefined; publishAndRetryFailedChallengeVerification(); } else { diff --git a/src/stores/accounts/accounts-database.test.ts b/src/stores/accounts/accounts-database.test.ts index b110426..48845f7 100644 --- a/src/stores/accounts/accounts-database.test.ts +++ b/src/stores/accounts/accounts-database.test.ts @@ -943,6 +943,42 @@ describe("accounts-database", () => { expect(await commentsDb.getItem("__storageVersion")).toBe(2); }); + test("getAccountComments densifies sparse legacy comment slots during compaction", async () => { + const acc = makeAccount({ id: "legacy-sparse-comments", name: "LegacySparseComments" }); + await accountsDatabase.addAccount(acc); + const commentsDb = createPerAccountDatabase("accountComments", acc.id); + await commentsDb.setItem("0", { + cid: "legacy-sparse-comment-1", + content: "legacy-1", + communityAddress: "sub", + timestamp: 1, + author: { address: "addr" }, + }); + await commentsDb.setItem("2", { + cid: "legacy-sparse-comment-2", + content: "legacy-2", + communityAddress: "sub", + timestamp: 2, + author: { address: "addr" }, + raw: { comment: { content: "legacy-2" } }, + }); + await commentsDb.setItem("length", 3); + + const comments = await accountsDatabase.getAccountComments(acc.id); + + expect(comments).toHaveLength(2); + expect(comments[0].cid).toBe("legacy-sparse-comment-1"); + expect(comments[1].cid).toBe("legacy-sparse-comment-2"); + expect(await commentsDb.getItem("1")).toEqual( + expect.objectContaining({ + cid: "legacy-sparse-comment-2", + }), + ); + expect(await commentsDb.getItem("2")).toBeNull(); + expect(await commentsDb.getItem("length")).toBe(2); + expect(await commentsDb.getItem("__storageVersion")).toBe(2); + }); + test("deleteAccountComment compacts legacy stored comments before mutating", async () => { const acc = makeAccount({ id: "legacy-delete-comments", name: "LegacyDeleteComments" }); await accountsDatabase.addAccount(acc); diff --git a/src/stores/accounts/accounts-database.ts b/src/stores/accounts/accounts-database.ts index 150b400..27961c2 100644 --- a/src/stores/accounts/accounts-database.ts +++ b/src/stores/accounts/accounts-database.ts @@ -376,15 +376,19 @@ const ensureAccountCommentsDatabaseLayout = async (accountId: string) => { } const comments = await getDatabaseAsArray(accountCommentsDatabase); - const updatedComments = comments.map((comment) => - comment ? sanitizeStoredAccountComment(comment) : comment, - ); + const updatedComments = comments + .map((comment) => (comment ? sanitizeStoredAccountComment(comment) : undefined)) + .filter((comment) => comment !== undefined); const rewritePromises: Promise[] = []; for (const [index, updatedComment] of updatedComments.entries()) { if (!isEqual(updatedComment, comments[index])) { rewritePromises.push(accountCommentsDatabase.setItem(String(index), updatedComment)); } } + for (let index = updatedComments.length; index < comments.length; index++) { + rewritePromises.push(accountCommentsDatabase.removeItem(String(index))); + } + rewritePromises.push(accountCommentsDatabase.setItem("length", updatedComments.length)); await Promise.all(rewritePromises); await accountCommentsDatabase.setItem(storageVersionKey, commentStorageVersion); })().finally(() => {