Skip to content

perf(accounts): slim account history storage and query hot paths#42

Merged
tomcasaburi merged 4 commits intomasterfrom
fix/account-history-performance
Mar 20, 2026
Merged

perf(accounts): slim account history storage and query hot paths#42
tomcasaburi merged 4 commits intomasterfrom
fix/account-history-performance

Conversation

@tomcasaburi
Copy link
Member

@tomcasaburi tomcasaburi commented Mar 19, 2026

Reduce account-history bloat by storing leaner account comments, deduplicating vote/edit persistence, and adding non-breaking query paths for hot account lookups. This keeps full account export/import behavior intact while letting apps avoid the worst local scans.

Closes #41


Note

Medium Risk
Touches persistence/migration logic for account comments, votes, and edits (including new indexes and lazy hydration), so regressions could affect local history integrity or query correctness across upgrades.

Overview
Reduces account-history storage bloat by sanitizing persisted account comments (dropping functions/sensitive fields and heavy replies.pages) and introducing compact, indexed persistence for votes/edits with migration/repair for legacy and partially-written DB states.

Expands useAccountComments, useAccountVotes, and useAccountComment with non-breaking query helpers (by commentCid, commentIndices, communityAddress, parentCid, vote, newerThan, pagination, and sortType/deprecated order) and adds per-account comment indexes to avoid full scans.

Keeps edit history cold by default: store init loads accountsEditsSummaries while accountsEdits are lazily hydrated via ensureAccountEditsLoaded, and useEditedComment now prefers summaries when full edits aren’t loaded. Publish-session tracking for pending comments is hardened to prevent deleted/reindexed pending items from being resurrected or receiving later event updates.

Written by Cursor Bugbot for commit 7feb65d. This will update automatically on new commits. Configure here.

Summary by CodeRabbit

  • New Features

    • More powerful account comment/vote queries: filter by community, parent, CID, index list, recency, page/size, and sort direction; fetch a single comment by CID or index.
  • Documentation

    • README updated with examples for direct CID/index retrieval, pagination, and recency queries.
  • Chores

    • Improved stored-data sanitization, migration/index rebuilds, and lazy-loading of full edit payloads while keeping summary data available.
  • Tests

    • Expanded coverage for queries, lazy edit loading, migrations, publishing/session flows, and state edge cases.

Preserve full backup compatibility while reducing account history bloat and lookup overhead.
@coderabbitai
Copy link

coderabbitai bot commented Mar 19, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Adds per-account comment indexes and edit summaries, sanitizes persisted account history, introduces lazy-loading for account edits, extends comment/vote hook query APIs (CID/index scoping, pagination, sorting, timestamp filters), and updates store/database/actions to maintain new indexes and summaries.

Changes

Cohort / File(s) Summary
Documentation
README.md
Updated docs and examples: useAccountComment, useAccountComments, useAccountVotes now accept CID/index scoping, pagination, sort, and timestamp options instead of callback-only filters.
Hooks
src/hooks/accounts/accounts.ts, src/hooks/accounts/accounts.test.ts
Expanded hook option shapes; useAccountComment resolves by commentIndex or commentCid; useAccountComments/useAccountVotes support scoping, filters, sort, pagination; added haveAccountCommentStatesChanged; tests added/extended for new behaviors.
Store (Zustand)
src/stores/accounts/accounts-store.ts, src/stores/accounts/accounts-store.test.ts
State extended with accountsCommentsIndexes, accountsEditsSummaries, accountsEditsLoaded; init loads summaries (lazy edits); reset clears lazy loaders; tests updated.
Actions / Internal
src/stores/accounts/accounts-actions.ts, src/stores/accounts/accounts-actions.test.ts, src/stores/accounts/accounts-actions-internal.ts, src/stores/accounts/accounts-actions-internal.test.ts
Publish-session switched to UUID sessions; added edit-summary helpers and sanitizers; added deduplicated lazy loader ensureAccountEditsLoaded + resetLazyAccountHistoryLoaders; maintained accountsCommentsIndexes recalculation; expanded tests for many branches.
Database Layer
src/stores/accounts/accounts-database.ts, src/stores/accounts/accounts-database.test.ts
Per-account DB naming, versioned layouts and internal index keys (__commentCidToLatestIndex, __targetToIndices, __summary); votes/edits rebuild logic; edits stored as numeric arrays with recomputed summaries; added getAccountEditsSummary(s) and sanitization of stored payloads; migration tests added.
Utilities
src/stores/accounts/utils.ts, src/stores/accounts/utils.test.ts
New helpers: sanitizeStoredAccountComment, getAccountCommentsIndex, getAccountsCommentsIndexes, getAccountEditPropertySummary, getAccountsEditsSummary; comprehensive tests for sanitization, indexing, and summary building.
Types
src/types.ts
Extended hook option types (UseAccountCommentOptions, UseAccountCommentsOptions, UseAccountVotesOptions) and added AccountCommentsIndex, AccountsCommentsIndexes, AccountEditPropertySummary, AccountEditsSummary, AccountsEditsSummaries.
Tests (misc)
src/stores/accounts/*.test.ts, src/hooks/accounts/*.test.ts
Many new and extended tests covering migrations, lazy-load merging, publish/edit/delete edge cases, indexing, summaries, and ordering/pagination behaviors.

Sequence Diagram(s)

sequenceDiagram
    participant Hook as useAccountEdits Hook
    participant Store as Accounts Store
    participant Actions as Actions/Internal
    participant DB as Database Layer
    participant Cache as EditLoad Cache

    Hook->>Store: read accountsEdits[accountId] (hot)
    alt accountsEditsLoaded[accountId] is false
        Hook->>Actions: ensureAccountEditsLoaded(accountId)
        Actions->>Cache: check accountEditsLoadPromises[accountId]
        alt promise exists
            Cache-->>Actions: return pending promise
        else
            Actions->>DB: getAccountEdits(accountId) (rebuild __targetToIndices/__summary)
            DB-->>Actions: edits + summaries
            Actions->>Store: merge edits, set accountsEditsSummaries, accountsEditsLoaded=true
            Actions->>Cache: resolve promise
        end
        Actions-->>Hook: notify succeeded
    end
    Store-->>Hook: return edits or summaries
Loading
sequenceDiagram
    participant Hook as useAccountComments Hook
    participant Store as Accounts Store
    participant Utils as IndexUtils

    Hook->>Store: request comments with options {commentCid?, commentIndices?, communityAddress?, parentCid?, newerThan?, page?, pageSize?, sortType?}
    Store->>Utils: lookup indices via accountsCommentsIndexes[accountId]
    Utils-->>Store: matched indices (by community/parent/indices/CID)
    Store->>Store: apply newerThan filter
    Store->>Store: apply sortType / order
    Store->>Store: apply page/pageSize slice
    Store-->>Hook: return filtered, sorted, paginated comment list
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Poem

🐰
I hopped through indexes and cleared the loft,
Kept CIDs neat and trimmed the payload soft.
Lazy-load whiskers fetch edits on demand,
Pages, sorts, and CIDs now answer when planned—
My burrow's tidy; big accounts feel light and oft.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main objective of slimming account history storage and optimizing query hot paths, which is the core focus of all changes.
Linked Issues check ✅ Passed All coding requirements from issue #41 are met: leaner storage via sanitization and migrations [README, accounts.ts, accounts-database.ts], indexed queries with pagination/filters [README, accounts.ts, types.ts], lazy edit hydration [accounts.ts, accounts-actions-internal.ts], and comprehensive test coverage validating these implementations.
Out of Scope Changes check ✅ Passed All changes directly support the stated objectives of reducing storage bloat and optimizing account history queries. No unrelated refactoring or feature additions outside issue #41's scope were detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/account-history-performance
📝 Coding Plan
  • Generate coding plan for human review comments

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/hooks/accounts/accounts.ts (1)

559-599: ⚠️ Potential issue | 🟠 Major

Sort votes by timestamp before applying order and pagination.

This array is built from a commentCid map, so its default order is object key order, not vote time. Re-voting an older commentCid keeps its original slot, which makes the new “recent votes” and paged order: "desc" queries return the wrong items.

⏱️ Suggested fix
     if (filter) {
       accountVotesArray = accountVotesArray.filter(filter);
     }
+    accountVotesArray = [...accountVotesArray].sort(
+      (a, b) => (a.timestamp || 0) - (b.timestamp || 0),
+    );
     if (order === "desc") {
       accountVotesArray.reverse();
     }
     if (typeof pageSize === "number" && pageSize > 0) {
       const pageNumber = Math.max(page || 0, 0);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/hooks/accounts/accounts.ts` around lines 559 - 599, The array built in
filteredAccountVotesArray should be sorted by vote timestamp before applying
order and pagination: after you finish applying filters to accountVotesArray
(but before the order === "desc" and pageSize slicing), call a deterministic
sort on timestamp (e.g., accountVotesArray.sort((a, b) => a.timestamp -
b.timestamp)) so recent/older ordering is correct; keep the existing order ===
"desc" reverse behavior (or alternatively sort by b.timestamp - a.timestamp when
order === "desc") and then apply pagination using page and pageSize as currently
implemented.
🧹 Nitpick comments (4)
src/stores/accounts/accounts-store.ts (1)

104-125: Add a note for the hot/cold edit split.

Preloading accountsEditsSummaries while intentionally initializing accountsEdits to empty objects is the key behavior change here, but it reads like an accidental omission without a short comment. A one-liner explaining that full edit history stays cold and is lazy-loaded would make this much easier to maintain.

As per coding guidelines, "Add comments for complex/non-obvious code; skip obvious comments."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/accounts/accounts-store.ts` around lines 104 - 125, Add a one-line
explanatory comment above the preloading/initialization block clarifying the
hot/cold edit split: note that accountsEditsSummaries is eagerly preloaded (hot)
while accountsEdits is intentionally initialized to empty objects and loaded
lazily (cold) to avoid fetching full edit histories; reference the
accountsEditsSummaries, accountsEdits, and accountsEditsLoaded symbols so future
readers understand this is intentional and not an omission.
src/stores/accounts/accounts-actions-internal.ts (1)

171-180: Reindex only the mutated account here.

Both call sites hand getAccountsCommentsIndexes(...) the full AccountsComments map just to replace one bucket. On large local histories, every single comment update/CID resolution becomes a full reindex.

♻️ Suggested narrowing
-    accountsStore.setState(({ accountsComments }) => {
+    accountsStore.setState(({ accountsComments, accountsCommentsIndexes }) => {
       // account no longer exists
       if (!accountsComments[account.id]) {
         log.error(
           `startUpdatingAccountCommentOnCommentUpdateEvents comment.on('update') invalid accountsStore.accountsComments['${account.id}'] '${
             accountsComments[account.id]
           }', account may have been deleted`,
         );
         return {};
       }

       const updatedAccountComments = [...accountsComments[account.id]];
       const previousComment = updatedAccountComments[currentIndex];
       const updatedAccountComment = utils.clone({
         ...storedUpdatedComment,
         index: currentIndex,
         accountId: account.id,
       });
       updatedAccountComments[currentIndex] = updatedAccountComment;
       const nextAccountsComments = {
         ...accountsComments,
         [account.id]: updatedAccountComments,
       };
+      const nextAccountCommentsIndexes =
+        getAccountsCommentsIndexes({ [account.id]: updatedAccountComments } as AccountsComments)[
+          account.id
+        ];
       return {
         accountsComments: nextAccountsComments,
         accountsCommentsIndexes: {
-          ...accountsStore.getState().accountsCommentsIndexes,
-          [account.id]: getAccountsCommentsIndexes(nextAccountsComments)[account.id],
+          ...accountsCommentsIndexes,
+          [account.id]: nextAccountCommentsIndexes,
         },
       };
     });

Apply the same narrowing in addCidToAccountComment(...) below.

Also applies to: 292-296

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/accounts/accounts-actions-internal.ts` around lines 171 - 180, The
code reindexes the entire AccountsComments map by calling
getAccountsCommentsIndexes(nextAccountsComments) when only a single account
bucket changed; change this to reindex only the mutated account by computing the
index for account.id from the updatedAccountComments (or by calling
getAccountsCommentsIndexes with a map containing just [account.id]:
updatedAccountComments) and merge that single-entry result into
accountsCommentsIndexes; apply the same change in addCidToAccountComment(...) so
both locations avoid full-map reindexing.
src/stores/accounts/accounts-actions.test.ts (1)

1670-1690: Assert the summary bucket in this moderation-path test too.

This branch only verifies accountsEdits, but useEditedComment() can now serve from accountsEditsSummaries before lazy edit hydration runs. A broken summary write would still pass here.

🧪 Suggested assertion
       expect(accountsStore.getState().accountsEdits[account.id].cid).toHaveLength(1);
+      expect(accountsStore.getState().accountsEditsSummaries[account.id]?.cid).toBeDefined();

Based on learnings: Maintain mandatory 100% test coverage for hooks and stores (src/hooks/, src/stores/); every feature or bug fix must include/adjust tests to keep coverage at 100%, and when changing hooks or stores, consider whether existing tests cover the change; if not, add a test.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/accounts/accounts-actions.test.ts` around lines 1670 - 1690, The
test for publishCommentModeration only asserts accountsEdits but must also
assert the summary bucket is initialized; update the test around
accountsActions.publishCommentModeration to also check
accountsStore.getState().accountsEditsSummaries[account.id] (or the summary
structure used by useEditedComment) has been created and contains the expected
cid entry (e.g., .cid length equals 1) after the call so broken summary writes
fail the test; locate the assertion area in the test and add the corresponding
expect on accountsEditsSummaries for the same account.id.
src/stores/accounts/accounts-actions.ts (1)

130-229: Extract the edit-summary reducers into a dedicated module.

This adds another chunk of reducer logic to a store action file that is already far past the repo’s ~300-line guidance, and it overlaps with summary logic that src/stores/accounts/utils.ts already owns via getAccountEditPropertySummary(). Moving these helpers next to the existing account-edit utilities would keep the action layer thinner and reduce drift.

As per coding guidelines, "Keep files focused; split large stores or hooks when they exceed ~300 lines".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/accounts/accounts-actions.ts` around lines 130 - 229, The
reducer/summary helpers (doesStoredAccountEditMatch, sanitizeStoredAccountEdit,
accountEditNonPropertyNames, addStoredAccountEditSummaryToState,
removeStoredAccountEditSummaryFromState) should be extracted out of the actions
file and moved into the accounts utils module alongside
getAccountEditPropertySummary; inside that utils module, reuse
getAccountEditPropertySummary where appropriate, keep the same exported function
names, and update all imports/usages in the actions file to import these helpers
from the utils module so the action file shrinks and summary logic is
centralized.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/hooks/accounts/accounts.test.ts`:
- Around line 1323-1346: The test "useAccountEdits logs and keeps initializing
when lazy load rejects" mutates store state but only restores
accountsActionsInternal; capture the original accountsEditsLoaded value for the
active accountId (from store.default.getState().accountsEditsLoaded[accountId])
before setting it to false and then restore it in the finally block along with
accountsActionsInternal (i.e., setState with both accountsActionsInternal:
internal and accountsEditsLoaded: originalEditsLoaded or by restoring the
specific key [accountId] to its previous boolean) so the shared store is
returned to its prior state after the test.

In `@src/hooks/accounts/accounts.ts`:
- Around line 499-518: The hook useAccountComment currently reads
accountComment.state directly from the raw accountsComments entry so
published/pending comments fall back to "initializing"; restore the derived
status the same way useAccountComments does by synthesizing state when building
accountComment (use the resolvedCommentIndex / accountComments lookup used here)
rather than trusting accountComment.state — either call the existing helper that
computes comment state or replicate that logic at the end of the useMemo so
accountComment includes the correct derived state (e.g., published vs pending vs
initializing).

In `@src/stores/accounts/accounts-actions.ts`:
- Around line 572-577: The store update is incorrectly marking
accountsEditsLoaded true after an optimistic append, which can hide missing
historical edits; in the update logic that sets accountsEdits,
accountsEditsSummaries and accountsEditsLoaded (referenced as accountsEdits,
accountsEditsSummaries, accountsEditsLoaded in this diff and the same pattern
around the other occurrences), do not set accountsEditsLoaded to true after an
optimistic/local append—leave it false until a real load/merge completes or
perform a proper merge/hydration of the existing edits bucket before flipping
the flag so lazy loaders will trigger a DB read.
- Around line 897-917: saveCreatedAccountComment currently always writes using
the captured accountCommentIndex which can be stale after deleteComment
reindexes/abandons a publish; change the function to read the session's live
index before persisting (e.g. pull currentIndex from
accountsStore.getState().accountsCommentsIndexes[account.id] or compute via
getAccountCommentsIndex on the live comments array) and: if the live index is
undefined/marked-abandoned then no-op and do not call
accountsDatabase.addAccountComment; otherwise use that live index instead of the
captured accountCommentIndex when calling accountsDatabase.addAccountComment and
when writing state (keep savedOnce semantics but update to reflect successful
persistence against the live index). Ensure the same live-index is used for both
the DB call (accountsDatabase.addAccountComment) and the subsequent
accountsStore.setState update to avoid overwriting the wrong slot.
- Around line 199-229: The function removeStoredAccountEditSummaryFromState
currently computes nextTargetSummary from the pre-removal accountsEdits bucket,
so a rolled-back/failed edit can remain the summary; instead, build the
editsForTarget as the accountsEdits[accountId]?.[editTarget] list with the
storedAccountEdit removed (filtering by a unique identifier present on
storedAccountEdit such as txHash/commentCid/cid or strict object inequality),
then pass that filtered list into getAccountEditPropertySummary to derive
nextTargetSummary and continue updating nextAccountSummary as before
(references: removeStoredAccountEditSummaryFromState, accountsEdits, accountId,
storedAccountEdit, getAccountEditPropertySummary, editTarget).
- Around line 152-229: Tests are missing for the new summary/lazy-hydration and
rollback/publish branches affecting store behavior; add unit tests that exercise
addStoredAccountEditSummaryToState and removeStoredAccountEditSummaryFromState
to verify summary add/remove semantics (including commentModeration
normalization), cover rollback after terminal challenge verification by
simulating edits and ensuring summaries update on rollback, and validate
accountsEditsLoaded behavior for imported accounts by initializing accountsEdits
and accountsEditsSummaries and asserting lazy-hydration populates expected
summaries via getAccountEditPropertySummary; include cases for imported
accounts, timestamp ordering, and deletion of empty targets.
- Around line 385-409: The deletion sequence removes per-account structures but
leaves commentCidsToAccountsComments stale; update the code that prepares state
for accountsStore.setState to also rebuild commentCidsToAccountsComments (or
remove any entries referencing account.id) — e.g., after creating
newAccountsComments (and before calling accountsStore.setState), iterate
newAccountsComments to reconstruct commentCidsToAccountsComments from the
remaining accounts' comment arrays (or filter existing
commentCidsToAccountsComments to drop mappings for account.id) so CID-based
lookups no longer return the deleted account.

In `@src/stores/accounts/accounts-database.ts`:
- Around line 593-595: getAccountEditTarget now returns
communityAddress/subplebbitAddress as valid targets, but the
persistence/rollback guards still require edit.commentCid so community edits
aren't persisted or removable; update the persistence checks in the account
edits save/remove code paths (the functions that currently check
edit.commentCid) to instead call and validate getAccountEditTarget(edit) (or
check for any of edit.commentCid || edit.communityAddress ||
edit.subplebbitAddress) before proceeding, and use that resolved target when
constructing keys or performing lookups so community edits are persisted and can
be rolled back/removed.

In `@src/stores/accounts/utils.ts`:
- Around line 67-99: The function sanitizeStoredAccountComment currently
deep-clones the entire Comment via cloneWithoutFunctions(...) then deletes
replies.pages, which still forces cloning the expensive replies.pages subtree;
instead, remove or replace comment.replies.pages before calling
cloneWithoutFunctions so the heavy subtree is never traversed. Concretely,
create a shallow preprocessedComment from the input comment that omits
replies.pages (e.g., copy comment and, if comment.replies?.pages exists, copy
replies without pages or set replies.pages = undefined) and pass that
preprocessed object into cloneWithoutFunctions; keep the existing post-clone
cleanup logic for sanitizedComment.replies as-is.

---

Outside diff comments:
In `@src/hooks/accounts/accounts.ts`:
- Around line 559-599: The array built in filteredAccountVotesArray should be
sorted by vote timestamp before applying order and pagination: after you finish
applying filters to accountVotesArray (but before the order === "desc" and
pageSize slicing), call a deterministic sort on timestamp (e.g.,
accountVotesArray.sort((a, b) => a.timestamp - b.timestamp)) so recent/older
ordering is correct; keep the existing order === "desc" reverse behavior (or
alternatively sort by b.timestamp - a.timestamp when order === "desc") and then
apply pagination using page and pageSize as currently implemented.

---

Nitpick comments:
In `@src/stores/accounts/accounts-actions-internal.ts`:
- Around line 171-180: The code reindexes the entire AccountsComments map by
calling getAccountsCommentsIndexes(nextAccountsComments) when only a single
account bucket changed; change this to reindex only the mutated account by
computing the index for account.id from the updatedAccountComments (or by
calling getAccountsCommentsIndexes with a map containing just [account.id]:
updatedAccountComments) and merge that single-entry result into
accountsCommentsIndexes; apply the same change in addCidToAccountComment(...) so
both locations avoid full-map reindexing.

In `@src/stores/accounts/accounts-actions.test.ts`:
- Around line 1670-1690: The test for publishCommentModeration only asserts
accountsEdits but must also assert the summary bucket is initialized; update the
test around accountsActions.publishCommentModeration to also check
accountsStore.getState().accountsEditsSummaries[account.id] (or the summary
structure used by useEditedComment) has been created and contains the expected
cid entry (e.g., .cid length equals 1) after the call so broken summary writes
fail the test; locate the assertion area in the test and add the corresponding
expect on accountsEditsSummaries for the same account.id.

In `@src/stores/accounts/accounts-actions.ts`:
- Around line 130-229: The reducer/summary helpers (doesStoredAccountEditMatch,
sanitizeStoredAccountEdit, accountEditNonPropertyNames,
addStoredAccountEditSummaryToState, removeStoredAccountEditSummaryFromState)
should be extracted out of the actions file and moved into the accounts utils
module alongside getAccountEditPropertySummary; inside that utils module, reuse
getAccountEditPropertySummary where appropriate, keep the same exported function
names, and update all imports/usages in the actions file to import these helpers
from the utils module so the action file shrinks and summary logic is
centralized.

In `@src/stores/accounts/accounts-store.ts`:
- Around line 104-125: Add a one-line explanatory comment above the
preloading/initialization block clarifying the hot/cold edit split: note that
accountsEditsSummaries is eagerly preloaded (hot) while accountsEdits is
intentionally initialized to empty objects and loaded lazily (cold) to avoid
fetching full edit histories; reference the accountsEditsSummaries,
accountsEdits, and accountsEditsLoaded symbols so future readers understand this
is intentional and not an omission.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b0502053-980a-4503-a41b-d48d72081828

📥 Commits

Reviewing files that changed from the base of the PR and between 3264934 and 4a85fbc.

📒 Files selected for processing (13)
  • README.md
  • src/hooks/accounts/accounts.test.ts
  • src/hooks/accounts/accounts.ts
  • src/stores/accounts/accounts-actions-internal.ts
  • src/stores/accounts/accounts-actions.test.ts
  • src/stores/accounts/accounts-actions.ts
  • src/stores/accounts/accounts-database.test.ts
  • src/stores/accounts/accounts-database.ts
  • src/stores/accounts/accounts-store.test.ts
  • src/stores/accounts/accounts-store.ts
  • src/stores/accounts/utils.test.ts
  • src/stores/accounts/utils.ts
  • src/types.ts

Comment on lines +152 to +229
export const addStoredAccountEditSummaryToState = (
accountsEditsSummaries: Record<string, Record<string, any>>,
accountId: string,
storedAccountEdit: any,
) => {
const editTarget =
storedAccountEdit.commentCid ||
storedAccountEdit.communityAddress ||
storedAccountEdit.subplebbitAddress;
if (!editTarget) {
return { accountsEditsSummaries };
}

const accountEditsSummary = accountsEditsSummaries[accountId] || {};
const targetSummary = accountEditsSummary[editTarget] || {};
const nextSummary = { ...targetSummary };
const normalizedEdit = storedAccountEdit.commentModeration
? { ...storedAccountEdit, ...storedAccountEdit.commentModeration, commentModeration: undefined }
: storedAccountEdit;

for (const propertyName in normalizedEdit) {
if (
normalizedEdit[propertyName] === undefined ||
accountEditNonPropertyNames.has(propertyName)
) {
continue;
}
const previousTimestamp = nextSummary[propertyName]?.timestamp || 0;
if ((normalizedEdit.timestamp || 0) >= previousTimestamp) {
nextSummary[propertyName] = {
timestamp: normalizedEdit.timestamp,
value: normalizedEdit[propertyName],
};
}
}

return {
accountsEditsSummaries: {
...accountsEditsSummaries,
[accountId]: {
...accountEditsSummary,
[editTarget]: nextSummary,
},
},
};
};

export const removeStoredAccountEditSummaryFromState = (
accountsEditsSummaries: Record<string, Record<string, any>>,
accountsEdits: Record<string, Record<string, any[]>>,
accountId: string,
storedAccountEdit: any,
) => {
const editTarget =
storedAccountEdit.commentCid ||
storedAccountEdit.communityAddress ||
storedAccountEdit.subplebbitAddress;
if (!editTarget) {
return { accountsEditsSummaries };
}

const nextTargetSummary = getAccountEditPropertySummary(
accountsEdits[accountId]?.[editTarget] || [],
);
const nextAccountSummary = { ...(accountsEditsSummaries[accountId] || {}) };
if (Object.keys(nextTargetSummary).length > 0) {
nextAccountSummary[editTarget] = nextTargetSummary;
} else {
delete nextAccountSummary[editTarget];
}

return {
accountsEditsSummaries: {
...accountsEditsSummaries,
[accountId]: nextAccountSummary,
},
};
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add store tests for the new summary and lazy-hydration branches.

This change adds new exported reducers, import-time lazy edit initialization, and rollback/publish branches in a src/stores/ file, but no matching tests were included. Please cover at least summary add/remove behavior, rollback after terminal challenge verification, and accountsEditsLoaded behavior for imported accounts.

As per coding guidelines, "Maintain mandatory 100% test coverage for hooks and stores (src/hooks/, src/stores/); every feature or bug fix must include/adjust tests to keep coverage at 100%" and "When changing hooks or stores, consider whether existing tests cover the change; if not, add a test".

Also applies to: 544-580, 1281-1488

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/accounts/accounts-actions.ts` around lines 152 - 229, Tests are
missing for the new summary/lazy-hydration and rollback/publish branches
affecting store behavior; add unit tests that exercise
addStoredAccountEditSummaryToState and removeStoredAccountEditSummaryFromState
to verify summary add/remove semantics (including commentModeration
normalization), cover rollback after terminal challenge verification by
simulating edits and ensuring summaries update on rollback, and validate
accountsEditsLoaded behavior for imported accounts by initializing accountsEdits
and accountsEditsSummaries and asserting lazy-hydration populates expected
summaries via getAccountEditPropertySummary; include cases for imported
accounts, timestamp ordering, and deletion of empty targets.

@tomcasaburi
Copy link
Member Author

Addressed the valid account-history review findings in the latest commits (38a67e3, fbe9c3d). Fixed the stale edit-summary rollback, restored derived useAccountComment() state, sorted vote queries by timestamp before pagination, kept accountsEditsLoaded cold during optimistic edit/moderation appends, used the live pending-comment index to avoid resurrecting deleted drafts, accepted community edit targets in persistence, cleaned up empty replies without cloning replies.pages, and added regression tests for those paths.

Local verification on the updated branch: yarn build ✅, yarn test ✅, yarn test:coverage ✅. The repo’s stricter node scripts/verify-hooks-stores-coverage.mjs gate still fails because hooks/stores are below 100% in the current repo-wide baseline, including files outside this PR scope, so I’m waiting on GitHub CI rather than merging ahead of the queued checks.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/stores/accounts/accounts-actions.ts (1)

50-240: Extract the new publish-session and edit-summary helpers into dedicated modules.

This PR adds two more non-trivial helper subsystems to an already very large store file. Pulling them out would make the publish/delete race handling and edit-summary rules much easier to test and reason about.

As per coding guidelines, "Keep files focused; split large stores or hooks when they exceed ~300 lines".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/stores/accounts/accounts-actions.ts` around lines 50 - 240, Split the
large accounts-actions file by extracting the publish-session logic
(activePublishSessions, abandonedPublishKeys, getPublishSessionKey,
registerPublishSession, abandonAndStopPublishSession, isPublishSessionAbandoned,
getPublishSessionByIndex, getPublishSessionForComment,
shiftPublishSessionIndicesAfterDelete, cleanupPublishSessionOnTerminal) into a
new module (e.g., publishSessions) exporting those functions and the Map/Set
accessors, and extract the edit-summary helpers (accountEditNonPropertyNames,
getStoredAccountEditTarget, addStoredAccountEditSummaryToState,
removeStoredAccountEditSummaryFromState, sanitizeStoredAccountEdit,
doesStoredAccountEditMatch, getAccountEditPropertySummary if referenced) into a
separate module (e.g., editSummaries) exporting the functions; update the
original file to import these symbols and replace local definitions with the
imports, adjust any internal references (e.g., maybeUpdateAccountComment still
in store) and ensure all callers compile, then add unit tests for
publish-session race behaviors and edit-summary rules in their new modules.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/stores/accounts/accounts-actions-internal.ts`:
- Around line 384-390: The load handler for loadPromise is replacing the
per-account edits bucket and clobbering optimistic local edits; instead of
setting accountsEdits: { ...accountsEdits, [accountId]: accountEdits } replace
that assignment in accountsStore.setState with a merge of the existing bucket
and the fetched snapshot so local optimistic entries from publishCommentEdit /
publishCommentModeration are preserved. Concretely, inside the then(...) for
accountsDatabase.getAccountEdits(accountId) read the current
accountsEdits[accountId] from the state, produce a merged list/map that unions
items by edit id (or uses timestamp to keep the newer entry), and write
accountsEdits: { ...accountsEdits, [accountId]: mergedBucket } while still
setting accountsEditsLoaded[accountId] = true.

In `@src/stores/accounts/accounts-actions.ts`:
- Around line 58-65: The publish session Map is using a mutable array index as
the key which can be reused after deletes and cause sessions to be overwritten;
change to a stable, immutable session identifier: update registerPublishSession
to generate or accept a stable sessionId (e.g., a UUID or a composite of
accountId + originalCommentId) and use that sessionId as the Map key instead of
index, store the original index inside the session object (currentIndex) for
position tracking, and update all usages that call getPublishSessionKey,
activePublishSessions.get/set/delete, and any cleanup logic (e.g., the code
referenced around registerPublishSession and the block at 121-128) to operate on
the new stable sessionId rather than the mutable index so sessions cannot be
accidentally reused.

In `@src/stores/accounts/accounts-database.ts`:
- Around line 621-641: The code is compacting sparse edit slots by calling
getDatabaseAsArray(...).filter(Boolean) which renumbers in-memory indices and
corrupts the mapping persisted via persistAccountEditsIndexes (used later by
getAccountEdits). In ensureAccountEditsDatabaseLayout, stop calling
.filter(Boolean); keep the array with holes so original numeric indices are
preserved when computing duplicateKeysToDelete and when calling
persistAccountEditsIndexes, or alternatively perform a full rewrite that renames
numeric DB keys to consecutive indices and update the DB before persisting
editsTargetToIndicesKey; update both the block in
ensureAccountEditsDatabaseLayout (using getAccountEditsDatabase,
getDatabaseAsArray, duplicateKeysToDelete, persistAccountEditsIndexes) and the
similar later block so numeric positions remain consistent with stored indices.

---

Nitpick comments:
In `@src/stores/accounts/accounts-actions.ts`:
- Around line 50-240: Split the large accounts-actions file by extracting the
publish-session logic (activePublishSessions, abandonedPublishKeys,
getPublishSessionKey, registerPublishSession, abandonAndStopPublishSession,
isPublishSessionAbandoned, getPublishSessionByIndex,
getPublishSessionForComment, shiftPublishSessionIndicesAfterDelete,
cleanupPublishSessionOnTerminal) into a new module (e.g., publishSessions)
exporting those functions and the Map/Set accessors, and extract the
edit-summary helpers (accountEditNonPropertyNames, getStoredAccountEditTarget,
addStoredAccountEditSummaryToState, removeStoredAccountEditSummaryFromState,
sanitizeStoredAccountEdit, doesStoredAccountEditMatch,
getAccountEditPropertySummary if referenced) into a separate module (e.g.,
editSummaries) exporting the functions; update the original file to import these
symbols and replace local definitions with the imports, adjust any internal
references (e.g., maybeUpdateAccountComment still in store) and ensure all
callers compile, then add unit tests for publish-session race behaviors and
edit-summary rules in their new modules.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: efbd8c6a-7a8a-482a-ba38-8930820b9d91

📥 Commits

Reviewing files that changed from the base of the PR and between 4a85fbc and fbe9c3d.

📒 Files selected for processing (11)
  • README.md
  • src/hooks/accounts/accounts.test.ts
  • src/hooks/accounts/accounts.ts
  • src/stores/accounts/accounts-actions-internal.ts
  • src/stores/accounts/accounts-actions.test.ts
  • src/stores/accounts/accounts-actions.ts
  • src/stores/accounts/accounts-database.test.ts
  • src/stores/accounts/accounts-database.ts
  • src/stores/accounts/accounts-store.ts
  • src/stores/accounts/utils.ts
  • src/types.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/stores/accounts/accounts-store.ts
  • src/stores/accounts/accounts-actions.test.ts
  • src/hooks/accounts/accounts.test.ts
  • src/hooks/accounts/accounts.ts

@tomcasaburi tomcasaburi merged commit 4d1d01c into master Mar 20, 2026
4 of 7 checks passed
@tomcasaburi tomcasaburi deleted the fix/account-history-performance branch March 20, 2026 06:02
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

};
const state = storedAccountComment
? getAccountCommentsStates([storedAccountComment])[0]
: "initializing";
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

useAccountComment loses periodic pending-to-failed state recheck

Medium Severity

The refactored useAccountComment computes state directly via getAccountCommentsStates at render time but no longer goes through useAccountComments, which had a useInterval rechecking states every 60 seconds. Without that interval, a pending comment will never automatically transition to "failed" after the 20-minute timeout because nothing triggers a re-render when only wall-clock time has changed.

Additional Locations (1)
Fix in Cursor Fix in Web

removeStoredAccountEditFromState(accountsEdits, account.id, storedCommentEdit),
);
return nextState;
}),
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Rollback recalculates edit summary from incomplete cold state

Medium Severity

removeStoredAccountEditSummaryFromState recalculates the target summary from accountsEdits, which is incomplete in cold/lazy state. Rolling back a failed edit wipes the summary entry for that target, losing information from older edits that still exist in the database but haven't been lazily loaded yet. Similarly, useEditedComment prefers getAccountEditPropertySummary(commentEdits) over the more complete commentEditSummary whenever commentEdits?.length > 0, which can be true from optimistic additions alone.

Additional Locations (1)
Fix in Cursor Fix in Web

const doesStoredAccountEditMatch = (storedAccountEdit: any, targetStoredAccountEdit: any) =>
storedAccountEdit?.clientId && targetStoredAccountEdit?.clientId
? storedAccountEdit.clientId === targetStoredAccountEdit.clientId
: isEqual(storedAccountEdit, targetStoredAccountEdit);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Triplicated doesStoredAccountEditMatch across three store files

Low Severity

The identical doesStoredAccountEditMatch function is independently defined in accounts-actions-internal.ts, accounts-actions.ts, and accounts-database.ts. If the matching logic needs updating (e.g., adding a new identity field), all three copies must be kept in sync — a likely source of future inconsistencies. One canonical export from a shared utility would eliminate the risk.

Additional Locations (2)
Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Account history storage and hooks do not scale for large accounts

1 participant