Skip to content

Implement a multi-layer offline-first cache with IndexedDB persistence, background sync, and conflict resolution for the creator profile and portfolio pages #754

Description

@Chucks1093

Summary

Users on slow or intermittent connections see spinners or blank pages when the network is unavailable. This issue implements a multi-layer cache: React Query as the in-memory L1 cache, IndexedDB as the L2 persistent cache (survives page reload and browser restart), and a background sync worker that pushes stale IndexedDB entries to the server when connectivity is restored — with a last-write-wins conflict resolution strategy for portfolio data.

Scope

1. IndexedDB adapter

  • Implement a IndexedDBCache class wrapping the browser's IndexedDB API with a typed interface matching React Query's QueryCache persistence contract
  • Store each query's { queryKey, data, dataUpdatedAt, queryHash } in an object store keyed by queryHash
  • Implement get(queryHash), set(queryHash, entry), delete(queryHash), and clear()
  • All operations must be non-blocking (use IndexedDB's async API) and must not throw — failures silently fall back to the in-memory cache

2. React Query persistence plugin

  • Wire the IndexedDBCache into React Query's persistQueryClient plugin so queries are automatically persisted to IndexedDB on every cache update
  • On app startup, restore all persisted queries younger than 24 hours before React Query's first network fetch so the UI renders immediately with stale data
  • Queries older than 24 hours are evicted from IndexedDB on startup

3. Background sync worker (Service Worker)

  • Register a Service Worker that intercepts failed GET requests to the creator profile and portfolio endpoints during offline periods
  • Queue failed requests in a sync-queue IndexedDB store tagged with { url, method, body, queuedAt }
  • When connectivity is restored, register a Background Sync event (sync tag: api-retry) and drain the queue — replay each request and update the IndexedDB cache with the fresh response
  • Discard queued requests older than 1 hour without replaying

4. Conflict resolution

  • On sync drain, if the server returns newer data (dataUpdatedAt > IndexedDB entry's dataUpdatedAt), replace the IndexedDB entry
  • If the IndexedDB entry is newer (user made local optimistic updates while offline), apply a last-write-wins merge: server data wins for all fields except optimisticFields which are preserved
  • optimisticFields is a per-query configuration list (e.g. for portfolio: ['positions'])

5. Offline indicator and stale banner

  • Show a persistent 'You are offline — showing cached data' banner when navigator.onLine is false
  • Show a 'Data may be outdated' badge on each data card when the IndexedDB entry age exceeds 5 minutes
  • Both indicators dismissed automatically when connectivity is restored and fresh data is loaded

6. Tests

  • Unit tests: IndexedDB adapter correctly stores, retrieves, and evicts entries
  • Unit tests: conflict resolution applies last-write-wins correctly for optimistic fields
  • Integration tests: app renders immediately with IndexedDB data on reload before any network response
  • Integration tests: queued requests are replayed and IndexedDB updated after connectivity restored

Acceptance Criteria

  • App renders with IndexedDB cached data before first network response on reload
  • Queries older than 24 hours evicted from IndexedDB on startup
  • Background sync replays queued requests when connectivity restored
  • Last-write-wins conflict resolution preserves optimistic fields
  • Offline banner and stale badge displayed correctly
  • IndexedDB adapter failures fall back silently to in-memory cache

ETA: 24 hours


Coordinate on Telegram

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