Skip to content

Fix/issue 78 like count drift clean#79

Open
1amKhush wants to merge 3 commits into
saiy2k:mainfrom
1amKhush:fix/issue-78-like-count-drift-clean
Open

Fix/issue 78 like count drift clean#79
1amKhush wants to merge 3 commits into
saiy2k:mainfrom
1amKhush:fix/issue-78-like-count-drift-clean

Conversation

@1amKhush

@1amKhush 1amKhush commented Apr 12, 2026

Copy link
Copy Markdown

Fixes #78

This PR fully resolves the like-count drift bug in failed like/unlike mutation paths.

When a like or unlike operation fails before optimistic state is applied, the UI no longer performs rollback arithmetic that can incorrectly increment or decrement the displayed count. The implementation now restores state deterministically and keeps count values non-negative.

Root Cause

The previous catch-path rollback assumed optimistic mutation had already been applied.If a failure occurred earlier (for example signer denial or publish failure), rollback logic could still run and drift local count.

What Changed

  • Added pure optimistic-state helpers in optimistic-state.ts

  • Updated mutation and failure handling in nostr-like.ts

  • Expanded regression coverage in optimistic-state.test.ts

Implementation Details

  • Like/unlike flows now snapshot pre-mutation UI state.

  • A local didApplyOptimisticUpdate flag controls rollback behavior.

  • Rollback is state-based (restore snapshot) rather than arithmetic.

  • Fetched relay totals are clamped before assigning to displayed count.

  • Added best-effort authoritative count resync after failed mutations, with an in-flight guard to prevent redundant resync calls.

Behavior After This Change

  • Failure-before-optimistic: local count does not drift.

  • Failure-after-optimistic: local state is restored to pre-mutation snapshot.

  • Displayed like count never drops below zero.

  • Failed mutations attempt relay-backed reconciliation to converge UI state.

Testing

Executed targeted regression tests:

  • npm run -s test -- optimistic-state.test.ts

Result:

  • 1 test file passed

  • 6 tests passed

Summary by CodeRabbit

  • New Features

    • Like button now applies optimistic like/unlike updates immediately and clamps counts to never go below zero.
    • Improved recovery: failed like/unlike attempts restore prior UI state and surface a clear error status; authoritative count resynchronization is queued to avoid repeated concurrent resyncs.
  • Tests

    • Added tests covering clamping, optimistic apply/rollback flows, and failure/resync scenarios.

Copilot AI review requested due to automatic review settings April 12, 2026 19:15
@coderabbitai

coderabbitai Bot commented Apr 12, 2026

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1fba98bd-3198-43db-8a68-75409382fbbc

📥 Commits

Reviewing files that changed from the base of the PR and between 5b741b0 and c2cd0a1.

📒 Files selected for processing (1)
  • src/nostr-like-button/nostr-like.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/nostr-like-button/nostr-like.ts

📝 Walkthrough

Walkthrough

Adds a new optimistic-state module and tests, integrates optimistic like/unlike handling into the like component, implements clamped counts, snapshot-based rollback that only reverts when an optimistic update was applied, and defers authoritative count resyncs after failures.

Changes

Cohort / File(s) Summary
Optimistic State Utilities
src/nostr-like-button/optimistic-state.ts
New exported LikeUiState and pure helpers: clampLikeCount, applyOptimisticLike, applyOptimisticUnlike, and rollbackOptimisticLikeState (snapshot + didApply flag).
Optimistic State Tests
src/nostr-like-button/__tests__/optimistic-state.test.ts
New Vitest suite covering clamping, optimistic like/unlike transitions, and rollback behavior for both applied and not-applied optimistic updates.
Like Component Integration
src/nostr-like-button/nostr-like.ts
Imports and uses optimistic utilities; captures snapshot at mutation time; applies optimistic updates and tracks didApplyOptimisticUpdate; centralizes failure handling in handleLikeMutationFailure() which rolls back, sets error status, and queues authoritative count resyncs; adds isResyncingLikeCount / needsResyncLikeCount and queueAuthoritativeCountResync().

Sequence Diagram(s)

sequenceDiagram
    participant UI as Client/UI
    participant Optim as Optimistic Module
    participant Pub as Publisher/Signer
    participant Relay as Relay/Server
    participant Store as Authoritative Count

    UI->>Optim: capture snapshot (LikeUiState)
    UI->>Optim: applyOptimisticLike/unlike -> updated UI state
    UI->>Pub: publish like/unlike mutation
    Pub->>Relay: send mutation
    alt mutation succeeds
        Relay->>Store: authoritative update
        Relay-->>UI: success
        UI->>UI: updateLikeCount(result.totalCount) [clamp]
    else mutation fails
        Relay-->>Pub: error
        Pub-->>UI: error
        UI->>Optim: rollbackOptimisticLikeState(current, snapshot, didApply)
        UI->>UI: restore isLiked/likeCount (clamped)
        UI->>Store: queueAuthoritativeCountResync()
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Like button #47 — Integrates or modifies the same nostr-like component; changes to nostr-like.ts here are directly related to that component's implementation.

Poem

🐰
I hopped and nudged the like to grow,
A snapshot held me safe, just so.
If hops should fail and tumble down,
I stitch it back—no count will drown.
Sniff, nibble, bounce—resync in tow!

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Fix/issue 78 like count drift clean' references the issue but uses vague phrasing ('drift clean') that could be clearer about the specific change. Consider clarifying the title to better describe the fix, such as 'Prevent like count drift on failed mutations with snapshot-based rollback' or similar.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed All primary coding requirements from issue #78 are met: optimistic update tracking via flag, snapshot-based rollback instead of arithmetic, count clamping to zero minimum, authoritative resync with in-flight guard, and test coverage for both failure scenarios.
Out of Scope Changes check ✅ Passed All changes (optimistic-state module, mutation failure handling, resync logic, and tests) are directly scoped to addressing like count drift on failed mutations per issue #78; no unrelated modifications detected.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Pull request overview

Resolves #78 by making like/unlike failure handling deterministic and preventing like-count drift when optimistic updates were not actually applied.

Changes:

  • Added pure helpers for clamping, optimistic updates, and rollback decisioning (optimistic-state.ts).
  • Refactored like/unlike mutation error handling to use snapshot-based rollback and clamp fetched totals (nostr-like.ts).
  • Added unit tests covering clamp behavior and failure-before/after-optimistic rollback semantics (optimistic-state.test.ts).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
src/nostr-like-button/optimistic-state.ts Introduces pure helpers for clamping counts and performing deterministic rollback decisions.
src/nostr-like-button/nostr-like.ts Uses helpers to clamp relay totals and to rollback only when an optimistic update was applied; adds best-effort resync.
src/nostr-like-button/tests/optimistic-state.test.ts Adds regression tests for the optimistic-state helpers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/nostr-like-button/nostr-like.ts Outdated
Comment thread src/nostr-like-button/nostr-like.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/nostr-like-button/nostr-like.ts (1)

169-179: Coalesce resync requests instead of dropping them.

The boolean guard prevents concurrent refreshes, but it also discards any failure that happens while an earlier resync is still in flight. If that earlier fetch started before the later failure, its result can already be stale and no follow-up fetch will run. A small needsResync flag or loop would preserve the latest reconciliation request without allowing overlap.

♻️ One way to keep the latest resync request
 private isResyncingLikeCount = false;
+private needsLikeCountResync = false;

 private queueAuthoritativeCountResync(): void {
-  if (this.isResyncingLikeCount) return;
-
-  this.isResyncingLikeCount = true;
+  this.needsLikeCountResync = true;
+  if (this.isResyncingLikeCount) return;
+
+  this.isResyncingLikeCount = true;
   void (async () => {
       try {
-        await this.updateLikeCount();
+        while (this.needsLikeCountResync) {
+          this.needsLikeCountResync = false;
+          await this.updateLikeCount();
+        }
       } finally {
         this.isResyncingLikeCount = false;
       }
   })();
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/nostr-like-button/nostr-like.ts` around lines 169 - 179, The current
queueAuthoritativeCountResync uses isResyncingLikeCount to block concurrent runs
but drops any resync requests while a run is in-flight; change it to coalesce
requests by adding a needsResync flag and looping: when
queueAuthoritativeCountResync is called, set needsResync = true and if
isResyncingLikeCount return; otherwise set isResyncingLikeCount = true and run a
loop that clears needsResync, calls await this.updateLikeCount(), then if
needsResync is true continue another iteration, finally set isResyncingLikeCount
= false; reference and update the existing isResyncingLikeCount,
queueAuthoritativeCountResync, and updateLikeCount symbols.
src/nostr-like-button/__tests__/optimistic-state.test.ts (1)

11-55: Add one component-level regression test for the NostrLike wiring.

This suite proves the pure helper, but it would still pass if handleLike() / handleUnlike() set didApplyOptimisticUpdate at the wrong time or skipped the resync path. A mocked failure test around src/nostr-like-button/nostr-like.ts would cover the actual bug surface this PR is fixing.

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

In `@src/nostr-like-button/__tests__/optimistic-state.test.ts` around lines 11 -
55, Add a component-level regression test exercising the NostrLike component
wiring (src/nostr-like-button/nostr-like.ts) that simulates a failed server
mutation after an optimistic update: mount or render NostrLike, trigger
handleLike() (and/or handleUnlike()) to apply the optimistic update, mock the
mutation to reject, and assert that the component calls the rollback/resync path
and that didApplyOptimisticUpdate is interpreted correctly (i.e., state is
restored to the snapshot when didApplyOptimisticUpdate=true and current state is
clamped when didApplyOptimisticUpdate=false). Ensure the test mocks the
network/mutation layer used by NostrLike so the failure occurs after the
optimistic change, and assert final DOM/state matches the expected post-rollback
values.
🤖 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/nostr-like-button/nostr-like.ts`:
- Around line 165-204: The optimistic-update flag is being set only after await
ndkEvent.publish(), so the catch path that calls handleLikeMutationFailure
(which relies on didApplyOptimisticUpdate and rollbackOptimisticLikeState) is
nearly never exercised; move the code that applies the optimistic UI change and
flips didApplyOptimisticUpdate to before calling ndkEvent.publish(), or
alternatively allow updateLikeCount() errors to propagate (remove its internal
try/catch) so post-publish refresh failures hit the outer catch and trigger
handleLikeMutationFailure; update references in the publish flow and tests
accordingly (look for ndkEvent.publish, rollbackOptimisticLikeState,
didApplyOptimisticUpdate, updateLikeCount, handleLikeMutationFailure, and
queueAuthoritativeCountResync).

---

Nitpick comments:
In `@src/nostr-like-button/__tests__/optimistic-state.test.ts`:
- Around line 11-55: Add a component-level regression test exercising the
NostrLike component wiring (src/nostr-like-button/nostr-like.ts) that simulates
a failed server mutation after an optimistic update: mount or render NostrLike,
trigger handleLike() (and/or handleUnlike()) to apply the optimistic update,
mock the mutation to reject, and assert that the component calls the
rollback/resync path and that didApplyOptimisticUpdate is interpreted correctly
(i.e., state is restored to the snapshot when didApplyOptimisticUpdate=true and
current state is clamped when didApplyOptimisticUpdate=false). Ensure the test
mocks the network/mutation layer used by NostrLike so the failure occurs after
the optimistic change, and assert final DOM/state matches the expected
post-rollback values.

In `@src/nostr-like-button/nostr-like.ts`:
- Around line 169-179: The current queueAuthoritativeCountResync uses
isResyncingLikeCount to block concurrent runs but drops any resync requests
while a run is in-flight; change it to coalesce requests by adding a needsResync
flag and looping: when queueAuthoritativeCountResync is called, set needsResync
= true and if isResyncingLikeCount return; otherwise set isResyncingLikeCount =
true and run a loop that clears needsResync, calls await this.updateLikeCount(),
then if needsResync is true continue another iteration, finally set
isResyncingLikeCount = false; reference and update the existing
isResyncingLikeCount, queueAuthoritativeCountResync, and updateLikeCount
symbols.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3b2efcc6-d92d-446b-8596-36a230c4c562

📥 Commits

Reviewing files that changed from the base of the PR and between dfc22e5 and 5b741b0.

📒 Files selected for processing (3)
  • src/nostr-like-button/__tests__/optimistic-state.test.ts
  • src/nostr-like-button/nostr-like.ts
  • src/nostr-like-button/optimistic-state.ts

Comment thread src/nostr-like-button/nostr-like.ts
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.

Bug: optimistic rollback can drift like counts on failed like/unlike operations

2 participants