/** * Offline Manager * Handle offline queue, sync, and storage */ import AsyncStorage from '@react-native-async-storage/async-storage'; import NetInfo from '@react-native-community/netinfo'; const QUEUE_KEY = '@quixzoom:observationQueue'; const SYNC_INTERVAL = 30000; // 30 seconds /** * Add observation to offline queue */ export async function queueObservation(observation) { try { const queue = await getQueue(); const queuedObservation = { ...observation, id: `queued_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, queuedAt: new Date().toISOString(), retryCount: 0, }; queue.push(queuedObservation); await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(queue)); return queuedObservation.id; } catch (error) { console.error('Failed to queue observation:', error); throw error; } } /** * Get offline queue */ export async function getQueue() { try { const queueJson = await AsyncStorage.getItem(QUEUE_KEY); return queueJson ? JSON.parse(queueJson) : []; } catch (error) { console.error('Failed to get queue:', error); return []; } } /** * Remove observation from queue */ export async function removeFromQueue(observationId) { try { const queue = await getQueue(); const filtered = queue.filter(obs => obs.id !== observationId); await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(filtered)); return filtered.length; } catch (error) { console.error('Failed to remove from queue:', error); throw error; } } /** * Update observation in queue */ export async function updateInQueue(observationId, updates) { try { const queue = await getQueue(); const index = queue.findIndex(obs => obs.id === observationId); if (index !== -1) { queue[index] = { ...queue[index], ...updates }; await AsyncStorage.setItem(QUEUE_KEY, JSON.stringify(queue)); } } catch (error) { console.error('Failed to update queue:', error); } } /** * Clear queue */ export async function clearQueue() { try { await AsyncStorage.removeItem(QUEUE_KEY); } catch (error) { console.error('Failed to clear queue:', error); } } /** * Get queue count */ export async function getQueueCount() { const queue = await getQueue(); return queue.length; } /** * Check if device is online */ export async function isOnline() { const netInfo = await NetInfo.fetch(); return netInfo.isConnected && netInfo.isInternetReachable; } /** * Sync queue with server */ export async function syncQueue(apiClient) { try { const online = await isOnline(); if (!online) { console.log('Device is offline, skipping sync'); return { synced: 0, failed: 0 }; } const queue = await getQueue(); if (queue.length === 0) { return { synced: 0, failed: 0 }; } console.log(`Syncing ${queue.length} observations...`); let synced = 0; let failed = 0; for (const observation of queue) { try { // Attempt to upload await apiClient.observations.create(observation); await removeFromQueue(observation.id); synced++; } catch (error) { console.error(`Failed to sync observation ${observation.id}:`, error); // Increment retry count const newRetryCount = (observation.retryCount || 0) + 1; if (newRetryCount >= 5) { // Max retries reached, remove from queue await removeFromQueue(observation.id); console.log(`Observation ${observation.id} removed after max retries`); } else { await updateInQueue(observation.id, { retryCount: newRetryCount }); } failed++; } } console.log(`Sync complete: ${synced} synced, ${failed} failed`); return { synced, failed }; } catch (error) { console.error('Sync failed:', error); return { synced: 0, failed: 0, error: error.message }; } } /** * Start automatic sync */ export function startAutoSync(apiClient, interval = SYNC_INTERVAL) { console.log('Starting auto sync...'); // Initial sync syncQueue(apiClient); // Periodic sync const syncInterval = setInterval(() => { syncQueue(apiClient); }, interval); // Listen for network changes const unsubscribe = NetInfo.addEventListener(state => { if (state.isConnected && state.isInternetReachable) { console.log('Network restored, triggering sync...'); syncQueue(apiClient); } }); // Return cleanup function return () => { clearInterval(syncInterval); unsubscribe(); }; } /** * Get queue status */ export async function getQueueStatus() { const queue = await getQueue(); const total = queue.length; const pending = queue.filter(obs => (obs.retryCount || 0) === 0).length; const retrying = queue.filter(obs => (obs.retryCount || 0) > 0).length; return { total, pending, retrying, oldestObservation: total > 0 ? queue[0].queuedAt : null, }; }