Skip to content

[Data Integrity] Deterministic offline conflict resolution and idempotent sync convergence across IndexedDB queues #1144

Description

@RUKAYAT-CODER

Overview
Offline writes are queued in IndexedDB by src/services/offlineSync.ts and OfflineSyncManager.ts, replayed against the API by src/services/offlineApi.ts, with conflict handling delegated to src/lib/conflict/resolver.ts. The current design has three correctness gaps. First, replay is not idempotent — a mutation that is retried after a flaky network can apply twice on the server. Second, conflict detection is last-write-wins by wall-clock timestamp only, which is unsafe under clock drift between devices and has no per-record version. Third, a partial batch failure can leave the IndexedDB queue and the client store (src/store/synchronizationEngine.ts, src/store/persistenceLayer.ts) diverged, with no rollback and no resumable cursor across app restarts. The offline hooks (src/hooks/useOfflineMode.tsx, src/hooks/useOfflineSync.ts) also expose no deterministic conflict state to the UI, so src/components/ConflictResolver.tsx cannot reliably present conflicts.

This issue makes offline sync deterministic and convergent: per-record versioning with a logical clock, idempotent replay with operation ids and server dedupe, transactional all-or-nothing batch drain with a resumable cursor, and a UI-facing conflict state. It is a cross-cutting change spanning ~14 files and ~950 lines including tests.

Specifications
Features:

Per-record versioning (monotonic version + updatedBy + a logical clock) on offline records, replacing pure timestamp LWW with a deterministic per-entity-type merge strategy that is stable regardless of device clock drift
Idempotent replay using a client-generated operation id plus server ack/dedupe, with capped exponential backoff on retry and a dead-letter queue for operations that exhaust retries
Transactional batch drain that is all-or-nothing per batch with rollback, so the queue and the store never diverge, plus a resumable cursor that survives app restarts
Conflict state surfaced to the UI via useOfflineSync (pending / conflicted / resolved) and consumed by the ConflictResolver component, with a retention/GC policy for successfully synced records

Tasks:

Extend conflict/resolver.ts and conflict/types.ts with version vectors and per-type resolution strategies, and add unit tests asserting merge determinism across reordered/clock-skewed inputs
Add operation ids and a dedupe map in offlineSync, and make offlineApi replay idempotent with ack handling and dead-lettering
Implement transactional batch drain with rollback and a resumable cursor in offlineSync/OfflineSyncManager
Reconcile the store through synchronizationEngine/persistenceLayer after each drain, and expose conflict state through useOfflineMode/useOfflineSync
Add retention/GC for acked records and a serviceWorker background-sync trigger to drain when connectivity returns
Write tests for idempotent replay under duplicate delivery, deterministic merge across clock drift, batch rollback on partial failure, and cursor resume after restart

Impacted Files:

src/services/offlineSync.ts
src/services/offlineApi.ts
OfflineSyncManager.ts
src/lib/conflict/resolver.ts
src/lib/conflict/types.ts
src/lib/queue/index.ts
src/store/synchronizationEngine.ts
src/store/persistenceLayer.ts
src/hooks/useOfflineMode.tsx
src/hooks/useOfflineSync.ts
src/components/ConflictResolver.tsx
src/serviceWorker.ts
src/constants/app.constants.ts
src/services/tests/offlineSync.test.ts (new)

Acceptance Criteria
Duplicate delivery of the same offline operation applies exactly once (idempotent replay is verified by a test)
Conflict resolution is deterministic across clock drift using version vectors, and per-entity strategies are documented and tested
A partial batch failure rolls back cleanly with no divergence between the IndexedDB queue and the store, and the drain resumes from a cursor after an app restart
The UI receives pending / conflicted / resolved states via the offline hooks, and acked records are garbage-collected per the retention policy
Tests cover idempotency, deterministic merge, batch rollback, and cursor resume, with >80% coverage on offlineSync.ts and resolver.ts

Metadata

Metadata

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third Campaign

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions