Offline Mode v0.8.0 - nself-org/nchat GitHub Wiki
Complete offline support with background synchronization for nself-chat.
The offline mode system provides comprehensive support for working without an internet connection, including:
- Message Caching: Last 1000 messages per channel
- Attachment Caching: Configurable size limits (default 100MB)
- Offline Queue: Queue operations while offline for later sync
- Background Sync: iOS (15-min intervals) and Android (WorkManager)
- Conflict Resolution: Last-write-wins and merge strategies
- Battery Optimization: Respects low battery and charging states
┌─────────────────────────────────────────────────┐
│ Application Layer │
│ (React Components + Hooks) │
└──────────────────┬──────────────────────────────┘
│
┌──────────────────▼──────────────────────────────┐
│ Offline Manager │
│ - Network Detection │
│ - Sync Coordination │
│ - Queue Management │
└──────────────────┬──────────────────────────────┘
│
┌────────────┼────────────┐
│ │ │
┌─────▼─────┐ ┌───▼───┐ ┌─────▼─────┐
│ Storage │ │ Sync │ │ Conflict │
│ Manager │ │ Queue │ │ Resolver │
└───────────┘ └───────┘ └───────────┘
│ │ │
┌─────▼────────────▼────────────▼─────┐
│ IndexedDB Storage │
│ - Messages (1000/channel) │
│ - Channels │
│ - Users │
│ - Queue │
│ - Attachments (100MB limit) │
└──────────────────────────────────────┘
IndexedDB wrapper for offline data persistence.
import { messageStorage, channelStorage, queueStorage } from '@/lib/offline/offline-storage'
// Save messages
await messageStorage.save({
id: 'msg-1',
channelId: 'channel-1',
content: 'Hello!',
senderId: 'user-1',
senderName: 'John',
createdAt: new Date(),
reactions: [],
attachments: [],
})
// Get last 1000 messages for a channel
const messages = await messageStorage.getByChannel('channel-1', 1000)
// Queue an action while offline
await queueStorage.add({
id: 'queue-1',
type: 'send_message',
payload: { channelId: 'channel-1', content: 'Offline message' },
priority: 'high',
status: 'pending',
// ...
})Orchestrates sync operations with the server.
import { getSyncManager } from '@/lib/offline/sync-manager'
const syncManager = getSyncManager({
autoSync: true,
syncInterval: 30000, // 30 seconds
syncOnReconnect: true,
batteryThreshold: 20, // Don't sync below 20%
})
// Initialize
await syncManager.initialize()
// Perform incremental sync (only new data)
const result = await syncManager.incrementalSync()
// Perform full sync (all data)
const fullResult = await syncManager.fullSync()
// Sync specific channel
const channelResult = await syncManager.syncChannel('channel-1')
// Listen to sync events
syncManager.subscribe((event) => {
console.log('Sync event:', event.type, event.data)
})Handles data conflicts during sync.
import { getConflictResolver } from '@/lib/offline/conflict-resolver'
const resolver = getConflictResolver()
// Set user choice callback for complex conflicts
resolver.setUserChoiceCallback(async (conflict) => {
// Show UI to user for manual resolution
const choice = await showConflictDialog(conflict)
return choice
})
// Resolve conflict automatically
const resolution = await resolver.autoResolve(conflict)
if (resolution.resolved) {
// Use resolved result
await save(resolution.result)
} else if (resolution.needsUserInput) {
// Show UI for manual resolution
await showConflictUI(conflict)
}Manages offline file caching with LRU eviction.
import { getAttachmentCache } from '@/lib/offline/attachment-cache'
const cache = getAttachmentCache({
maxSize: 100 * 1024 * 1024, // 100MB
maxFileSize: 25 * 1024 * 1024, // 25MB per file
generateThumbnails: true,
})
await cache.initialize()
// Download and cache an attachment
const attachment = await cache.download(
'https://example.com/file.jpg',
{
id: 'att-1',
messageId: 'msg-1',
channelId: 'channel-1',
name: 'photo.jpg',
type: 'image/jpeg',
size: 1024000,
},
(progress) => {
console.log(`Downloaded ${progress.percent}%`)
}
)
// Get cached attachment
const cached = await cache.get('att-1')
if (cached) {
const dataUrl = await cache.getDataUrl('att-1')
// Use data URL in <img> tag
}
// Get cache statistics
const stats = await cache.getStats()
console.log(`Cache: ${stats.count} files, ${stats.usagePercent}% full`)Monitors network status and quality.
import { getNetworkDetector } from '@/lib/offline/network-detector'
const detector = getNetworkDetector()
// Subscribe to network changes
detector.subscribe((info) => {
console.log('Network state:', info.state)
console.log('Quality:', info.quality)
console.log('Connection type:', info.type)
console.log('RTT:', info.rtt)
})
// Check current status
const isOnline = detector.isOnline()
const quality = detector.getQuality()
const isSlow = detector.isSlowConnection()
// Start periodic connectivity check
detector.startPeriodicCheck(10000, '/api/health')Complete offline state management.
import { useOffline } from '@/hooks/use-offline';
function ChatView() {
const { state, actions } = useOffline();
return (
<div>
<p>Status: {state.isOnline ? 'Online' : 'Offline'}</p>
<p>Pending: {state.pendingCount}</p>
<p>Last sync: {state.lastSyncAt?.toLocaleString()}</p>
<button onClick={actions.syncNow} disabled={state.isSyncing}>
{state.isSyncing ? 'Syncing...' : 'Sync Now'}
</button>
<button onClick={actions.clearCache}>
Clear Cache
</button>
</div>
);
}Trigger sync operations.
import { useSync } from '@/hooks/use-sync';
function SyncButton() {
const { isSyncing, syncNow, state } = useSync();
return (
<button onClick={syncNow} disabled={isSyncing}>
{isSyncing ? `Syncing... ${state.progress}%` : 'Sync'}
</button>
);
}Shows offline status with pending changes.
import { OfflineIndicator, OfflineIndicatorCompact } from '@/components/ui/offline-indicator';
// Full indicator (expandable details)
<OfflineIndicator position="top" detailed={true} />
// Compact floating indicator
<OfflineIndicatorCompact />Visual progress indicator for sync operations.
import { SyncProgress, SyncProgressToast } from '@/components/ui/sync-progress';
// Full progress card
<SyncProgress detailed={true} />
// Overlay progress
<SyncProgress overlay={true} />
// Toast notification
<SyncProgressToast />15-minute interval background updates.
import { backgroundFetchService } from '@/lib/ios/background-fetch'
// Configure
await backgroundFetchService.configure({
minimumInterval: 900, // 15 minutes (minimum allowed by iOS)
stopOnTerminate: false,
enableHeadless: true,
})
// Start
await backgroundFetchService.start()
// Listen for fetch events
backgroundFetchService.onFetch('my-handler', (result) => {
console.log('Background fetch completed:', result)
if (result.newData) {
showNotification(`${result.messages} new messages`)
}
})Battery-efficient background jobs.
import { workManager } from '@/lib/android/work-manager'
// Initialize
await workManager.initialize({
enablePeriodicSync: true,
syncIntervalMinutes: 15,
requiresCharging: false,
requiresWifi: false,
})
// Trigger immediate sync
await workManager.syncNow()
// Update sync interval
await workManager.updateSyncInterval(30) // 30 minutes
// Get sync statistics
const stats = await workManager.getSyncStats()
console.log('Last sync:', new Date(stats.lastSyncTime))const DEFAULT_OFFLINE_CONFIG = {
// Cache settings
cacheEnabled: true,
maxCacheSize: 50 * 1024 * 1024, // 50MB
maxCacheAge: 7 * 24 * 60 * 60 * 1000, // 7 days
cacheChannelMessages: 1000, // Messages per channel
cacheChannels: 50,
// Queue settings
queueEnabled: true,
maxQueueSize: 100,
maxQueueAge: 24 * 60 * 60 * 1000, // 24 hours
// Sync settings
autoSync: true,
syncInterval: 30 * 1000, // 30 seconds
syncOnReconnect: true,
backgroundSync: true,
// Retry settings
retry: {
maxRetries: 5,
baseDelay: 1000,
maxDelay: 30000,
strategy: 'exponential',
},
// Network settings
networkCheckInterval: 10000,
networkCheckUrl: '/api/health',
}- Enable airplane mode
- Send messages (should queue)
- Disable airplane mode
- Messages should sync automatically
- Throttle network to 3G
- Send messages
- Observe sync progress
- Verify all messages synced
- Switch between WiFi and cellular
- Send messages during transition
- Verify no message loss
- Check sync queue status
- Set battery to <20%
- Observe background sync pause
- Plug in charger
- Verify sync resumes
- Cache Access: <10ms average
- Sync Queue Add: <5ms
- Incremental Sync: <3s for 100 messages
- Conflict Resolution: <100ms per conflict
- Battery Impact: <5% per hour with background sync
import { getStorageStats } from '@/lib/offline/offline-storage'
const stats = await getStorageStats()
console.log('Storage stats:', {
channels: stats.channels,
messages: stats.messages,
users: stats.users,
queue: stats.queue,
size: `${(stats.estimatedSize / 1024 / 1024).toFixed(2)}MB`,
})- Limit to 1000 messages per channel
- Clean up old cache regularly
- Monitor storage usage
- Prioritize user-facing operations (messages > reactions)
- Set reasonable retry limits
- Handle permanent failures gracefully
- Use incremental sync by default
- Full sync only when necessary
- Batch operations for efficiency
- Respect battery saver mode
- Reduce sync frequency on low battery
- Pause sync when battery < 20%
- Use last-write-wins for simple conflicts
- Prompt user for important conflicts
- Maintain tombstone records for deletions
- Check network status:
detector.isOnline() - Check sync status:
syncManager.getState() - Check queue:
queueStorage.getPending() - Review error logs in sync events
- Check sync interval: reduce if needed
- Disable background sync when idle
- Respect battery threshold settings
- Check cache stats:
getStorageStats() - Clear old messages:
messageStorage.clear() - Reduce attachment cache size
- Clean up completed queue items
See TypeScript definitions in:
-
/src/lib/offline/offline-types.ts- All types and interfaces -
/src/lib/offline/offline-storage.ts- Storage API -
/src/lib/offline/sync-manager.ts- Sync API -
/src/lib/offline/conflict-resolver.ts- Conflict resolution API -
/src/lib/offline/attachment-cache.ts- Attachment cache API
- ✅ Complete offline storage with 1000-message caching
- ✅ Attachment caching with configurable limits
- ✅ Offline queue with priority management
- ✅ Incremental and full sync operations
- ✅ Conflict resolution (last-write-wins, merge)
- ✅ iOS background fetch integration
- ✅ Android WorkManager integration
- ✅ Battery-aware sync scheduling
- ✅ Network quality monitoring
- ✅ React hooks and UI components
- ✅ Comprehensive test suite
MIT