Skip to content

fix(sync): stop a single un-writable record from wedging the sync engine forever#254

Merged
RyRy79261 merged 1 commit into
mainfrom
claude/voice-input-sync-issue-27gj5z
Jun 29, 2026
Merged

fix(sync): stop a single un-writable record from wedging the sync engine forever#254
RyRy79261 merged 1 commit into
mainfrom
claude/voice-input-sync-issue-27gj5z

Conversation

@RyRy79261

@RyRy79261 RyRy79261 commented Jun 21, 2026

Copy link
Copy Markdown
Owner

Symptom: after a voice log, the app would appear to "sync forever" — every
save felt like it never settled and the status dot stayed yellow ("Syncing N
changes…") indefinitely.

Root cause: the push cycle treated every server rejection without
code: "invalid" as transient and only bumped its attempts counter — it was
never dropped and never rescheduled on its own. A permanently un-writable row
(a CHECK violation, or a column missing from the deployed Postgres table after
a skipped migration) therefore stayed in the queue forever. Because
queueDepth > 0 is one of the conditions the indicator reports as "syncing",
the engine looked perpetually busy and the op only ever got re-attempted when
a later manual write happened to flush the same batch.

Fixes:

  • Drop a non-"invalid" rejection once it has failed MAX_PUSH_ATTEMPTS (8)
    times (its local Dexie row is untouched), so the queue can reach empty and
    the indicator can settle. Whole-batch network/HTTP failures are unchanged —
    those retry forever so an outage never discards writes.
  • Schedule a backoff retry for bumped-but-not-dropped rejections so they keep
    trying autonomously instead of stalling until the next manual write.
  • Only invalidate React Query caches on a pull that actually applied rows.
    Every push chains a pull, so a multi-record voice log previously triggered a
    full refetch storm on no-op pulls, making analytics/insights views thrash.

Adds regression tests for the retry-cap drop and the below-cap bump paths.

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01J2ktncagiX7xm4mXpD2uQ8

Summary by CodeRabbit

  • Bug Fixes
    • Queued operations rejected by the server are now automatically dropped from the sync queue after 8 retry attempts, preventing indefinite retry loops
    • Synchronization cache refresh is now optimized to only occur when new data is actually applied, avoiding unnecessary updates on no-op synchronization cycles

…ine forever

Symptom: after a voice log, the app would appear to "sync forever" — every
save felt like it never settled and the status dot stayed yellow ("Syncing N
changes…") indefinitely.

Root cause: the push cycle treated every server rejection without
`code: "invalid"` as transient and only bumped its attempts counter — it was
never dropped and never rescheduled on its own. A permanently un-writable row
(a CHECK violation, or a column missing from the deployed Postgres table after
a skipped migration) therefore stayed in the queue forever. Because
`queueDepth > 0` is one of the conditions the indicator reports as "syncing",
the engine looked perpetually busy and the op only ever got re-attempted when
a later manual write happened to flush the same batch.

Fixes:
- Drop a non-"invalid" rejection once it has failed MAX_PUSH_ATTEMPTS (8)
  times (its local Dexie row is untouched), so the queue can reach empty and
  the indicator can settle. Whole-batch network/HTTP failures are unchanged —
  those retry forever so an outage never discards writes.
- Schedule a backoff retry for bumped-but-not-dropped rejections so they keep
  trying autonomously instead of stalling until the next manual write.
- Only invalidate React Query caches on a pull that actually applied rows.
  Every push chains a pull, so a multi-record voice log previously triggered a
  full refetch storm on no-op pulls, making analytics/insights views thrash.

Adds regression tests for the retry-cap drop and the below-cap bump paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J2ktncagiX7xm4mXpD2uQ8
@vercel

vercel Bot commented Jun 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
intake-tracker Ready Ready Preview, Comment Jun 21, 2026 10:17am

@coderabbitai

coderabbitai Bot commented Jun 21, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The sync engine gains a MAX_PUSH_ATTEMPTS (8) constant that permanently drops server-rejected push ops once they exhaust their retry budget. Rejection handling in runPushCycle is rewritten to distinguish permanent drops from transient retries. In runPullCycle, React Query cache invalidation is now skipped when no rows were written to Dexie. A new test covers the retry-cap drop path.

Changes

Sync Engine Improvements

Layer / File(s) Summary
Push cycle retry cap and test coverage
apps/web/src/lib/sync-engine.ts, apps/web/src/__tests__/sync-engine.test.ts
Exports MAX_PUSH_ATTEMPTS = 8; rewrites runPushCycle rejection handling to permanently drop ops at the cap or on code === "invalid", bump attempts and schedule nextBackoff() retries for transient rejections, and update the re-drain condition to key off droppedCount. Test seeds a queue row at MAX_PUSH_ATTEMPTS - 1 attempts, stubs a non-invalid rejection, runs runPushCycle(), and asserts queue entry removal, Dexie row preservation, and queueDepth === 0.
Pull cycle conditional React Query invalidation
apps/web/src/lib/sync-engine.ts
Introduces appliedAnyRows boolean, sets it on successful Dexie bulk writes, and gates queryClient.invalidateQueries() so no-op pull cycles skip cache invalidation.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐇 Hoppity-hop through the retry queue,
Eight attempts and then we're through!
No more loops that never end —
The depth drops back to zero, friend.
And when no rows land on the shelf,
React Query rests all by itself! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 fix: preventing a single un-writable record from indefinitely stalling the sync engine by introducing a retry cap mechanism.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ 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 claude/voice-input-sync-issue-27gj5z

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.

@github-actions

Copy link
Copy Markdown
Contributor

Coverage Report for apps/web

Status Category Percentage Covered / Total
🔵 Lines 56.32% (🎯 54%) 6685 / 11869
🔵 Statements 54.94% (🎯 53%) 7134 / 12984
🔵 Functions 45.93% (🎯 45%) 1480 / 3222
🔵 Branches 46.65% (🎯 44%) 4008 / 8590
Generated in workflow #475 for commit 2d30e27 by the Vitest Coverage Report Action

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
apps/web/src/__tests__/sync-engine.test.ts (1)

687-723: 💤 Low value

Consider adding coverage for the backoff scheduling behavior.

The test verifies the queue entry is kept and attempts is bumped, but doesn't assert that schedulePush is called with the appropriate backoff delay. This is one of the key behavioral changes noted in the PR objectives ("Schedule backoff retries for bumped-but-not-dropped rejections so they continue attempting autonomously").

Adding a spy on schedulePush and verifying it was called with the expected nextBackoff(1) value would strengthen confidence in this path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/web/src/__tests__/sync-engine.test.ts` around lines 687 - 723, The test
for the push cycle rejection handling verifies that the queue entry is kept and
attempts are bumped, but it does not validate that the backoff scheduling occurs
as intended. Add a spy on the schedulePush function before the runPushCycle()
call, then after runPushCycle() completes, add assertions to verify that
schedulePush was called with the expected nextBackoff(1) value. This will ensure
the backoff retry scheduling behavior for bumped-but-not-dropped rejections is
properly tested.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@apps/web/src/__tests__/sync-engine.test.ts`:
- Around line 687-723: The test for the push cycle rejection handling verifies
that the queue entry is kept and attempts are bumped, but it does not validate
that the backoff scheduling occurs as intended. Add a spy on the schedulePush
function before the runPushCycle() call, then after runPushCycle() completes,
add assertions to verify that schedulePush was called with the expected
nextBackoff(1) value. This will ensure the backoff retry scheduling behavior for
bumped-but-not-dropped rejections is properly tested.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 637db7de-a26c-4119-aed2-e0f6b99bd610

📥 Commits

Reviewing files that changed from the base of the PR and between fbd77aa and 2d30e27.

📒 Files selected for processing (2)
  • apps/web/src/__tests__/sync-engine.test.ts
  • apps/web/src/lib/sync-engine.ts

@RyRy79261 RyRy79261 merged commit 45ff32f into main Jun 29, 2026
23 checks passed
@RyRy79261 RyRy79261 deleted the claude/voice-input-sync-issue-27gj5z branch June 29, 2026 18:32
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.

2 participants