201 lines
4.9 KiB
JavaScript
201 lines
4.9 KiB
JavaScript
|
|
/**
|
||
|
|
* 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,
|
||
|
|
};
|
||
|
|
}
|