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
ETA: 24 hours
Coordinate on Telegram
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
IndexedDBCacheclass wrapping the browser's IndexedDB API with a typed interface matching React Query'sQueryCachepersistence contract{ queryKey, data, dataUpdatedAt, queryHash }in an object store keyed byqueryHashget(queryHash),set(queryHash, entry),delete(queryHash), andclear()2. React Query persistence plugin
IndexedDBCacheinto React Query'spersistQueryClientplugin so queries are automatically persisted to IndexedDB on every cache update3. Background sync worker (Service Worker)
sync-queueIndexedDB store tagged with{ url, method, body, queuedAt }synctag:api-retry) and drain the queue — replay each request and update the IndexedDB cache with the fresh response4. Conflict resolution
dataUpdatedAt> IndexedDB entry'sdataUpdatedAt), replace the IndexedDB entryoptimisticFieldswhich are preservedoptimisticFieldsis a per-query configuration list (e.g. for portfolio:['positions'])5. Offline indicator and stale banner
navigator.onLineis false6. Tests
Acceptance Criteria
ETA: 24 hours
Coordinate on Telegram