UNPKG

expo-geofencing

Version:

Production-ready geofencing and activity recognition for Expo React Native with offline support, security features, and enterprise-grade reliability

332 lines (331 loc) 12.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.OfflineManager = void 0; class OfflineManager { constructor() { this.geofences = new Map(); this.pendingEvents = []; this.networkStatus = { isConnected: false, connectionType: 'none', isExpensive: false, lastConnected: 0 }; this.syncCallbacks = []; this.syncInProgress = false; this.retryTimeouts = new Set(); this.setupNetworkMonitoring(); this.setupPeriodicSync(); } // Geofence Management addGeofence(geofence) { const offlineGeofence = { ...geofence, isActive: true, lastEvaluated: Date.now() }; this.geofences.set(geofence.id, offlineGeofence); } removeGeofence(regionId) { this.geofences.delete(regionId); } updateGeofence(regionId, updates) { const existing = this.geofences.get(regionId); if (existing) { this.geofences.set(regionId, { ...existing, ...updates }); } } getGeofences() { return Array.from(this.geofences.values()); } getActiveGeofences() { return this.getGeofences().filter(g => g.isActive); } // Offline Geofence Evaluation evaluateLocation(location) { const events = []; const activeGeofences = this.getActiveGeofences(); for (const geofence of activeGeofences) { const distance = this.calculateDistance(location.latitude, location.longitude, geofence.latitude, geofence.longitude); const isInside = distance <= geofence.radius; const wasInside = geofence.metadata?.wasInside || false; // Update evaluation timestamp geofence.lastEvaluated = location.timestamp; if (isInside && !wasInside && geofence.notifyOnEntry) { // Entry event const event = { id: this.generateEventId(), type: 'enter', regionId: geofence.id, location, timestamp: location.timestamp, synced: false }; events.push(event); this.pendingEvents.push(event); // Update state geofence.metadata = { ...geofence.metadata, wasInside: true }; } else if (!isInside && wasInside && geofence.notifyOnExit) { // Exit event const event = { id: this.generateEventId(), type: 'exit', regionId: geofence.id, location, timestamp: location.timestamp, synced: false }; events.push(event); this.pendingEvents.push(event); // Update state geofence.metadata = { ...geofence.metadata, wasInside: false }; } } // Trigger sync if we have network and events if (events.length > 0 && this.networkStatus.isConnected) { this.scheduleSyncAttempt(); } return events; } // Network Management updateNetworkStatus(status) { const wasConnected = this.networkStatus.isConnected; this.networkStatus = { ...this.networkStatus, ...status }; if (!wasConnected && this.networkStatus.isConnected) { // Network became available this.networkStatus.lastConnected = Date.now(); this.scheduleSyncAttempt(); } } getNetworkStatus() { return { ...this.networkStatus }; } // Sync Management addSyncCallback(callback) { this.syncCallbacks.push(callback); } removeSyncCallback(callback) { const index = this.syncCallbacks.indexOf(callback); if (index > -1) { this.syncCallbacks.splice(index, 1); } } async syncPendingEvents() { if (this.syncInProgress || !this.networkStatus.isConnected) { return { success: false, synced: 0, failed: 0 }; } this.syncInProgress = true; try { const unsyncedEvents = this.pendingEvents.filter(e => !e.synced); if (unsyncedEvents.length === 0) { return { success: true, synced: 0, failed: 0 }; } let syncedCount = 0; let failedCount = 0; // Batch events for efficient sync const batches = this.batchEvents(unsyncedEvents); for (const batch of batches) { try { // Call all sync callbacks await Promise.all(this.syncCallbacks.map(callback => callback(batch))); // Mark events as synced batch.forEach(event => { event.synced = true; syncedCount++; }); } catch (error) { console.error('Batch sync failed:', error); failedCount += batch.length; } } // Remove synced events from pending list this.pendingEvents = this.pendingEvents.filter(e => !e.synced); return { success: syncedCount > 0, synced: syncedCount, failed: failedCount }; } finally { this.syncInProgress = false; } } getPendingEvents() { return [...this.pendingEvents]; } getPendingEventsCount() { return this.pendingEvents.filter(e => !e.synced).length; } clearSyncedEvents() { this.pendingEvents = this.pendingEvents.filter(e => !e.synced); } // Force immediate sync attempt async forceSyncAttempt() { return this.syncPendingEvents(); } // Batch Management batchEvents(events, batchSize = 50) { const batches = []; for (let i = 0; i < events.length; i += batchSize) { batches.push(events.slice(i, i + batchSize)); } return batches; } // Retry Logic scheduleSyncAttempt(delay = 1000) { if (this.syncInProgress) return; const timeout = setTimeout(async () => { this.retryTimeouts.delete(timeout); try { const result = await this.syncPendingEvents(); // If sync failed and we still have pending events, schedule retry with backoff if (!result.success && this.getPendingEventsCount() > 0) { const nextDelay = Math.min(delay * 2, 60000); // Max 1 minute this.scheduleSyncAttempt(nextDelay); } } catch (error) { console.error('Sync attempt failed:', error); // Schedule retry with exponential backoff const nextDelay = Math.min(delay * 2, 60000); this.scheduleSyncAttempt(nextDelay); } }, delay); this.retryTimeouts.add(timeout); } // Utility Methods calculateDistance(lat1, lon1, lat2, lon2) { const R = 6371000; // Earth's radius in meters const dLat = this.toRadians(lat2 - lat1); const dLon = this.toRadians(lon2 - lon1); const a = Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.cos(this.toRadians(lat1)) * Math.cos(this.toRadians(lat2)) * Math.sin(dLon / 2) * Math.sin(dLon / 2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); return R * c; } toRadians(degrees) { return degrees * (Math.PI / 180); } generateEventId() { return `offline_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`; } // Network Monitoring Setup setupNetworkMonitoring() { // This would integrate with React Native's NetInfo // For now, we'll simulate network status updates if (typeof globalThis !== 'undefined' && globalThis.navigator) { const nav = globalThis.navigator; if (typeof globalThis.addEventListener === 'function') { globalThis.addEventListener('online', () => { this.updateNetworkStatus({ isConnected: true, connectionType: 'wifi' // Simplified assumption }); }); globalThis.addEventListener('offline', () => { this.updateNetworkStatus({ isConnected: false, connectionType: 'none' }); }); } // Initial status this.updateNetworkStatus({ isConnected: nav.onLine || true, connectionType: nav.onLine ? 'wifi' : 'none' }); } } // Periodic Sync Setup setupPeriodicSync() { // Attempt sync every 30 seconds if we have pending events setInterval(() => { if (this.getPendingEventsCount() > 0 && this.networkStatus.isConnected) { this.scheduleSyncAttempt(); } }, 30000); } // Advanced Features // Intelligent Batching Based on Network Quality getBatchSizeForNetwork() { if (!this.networkStatus.isConnected) return 0; if (this.networkStatus.connectionType === 'wifi') { return 100; // Larger batches on WiFi } else if (this.networkStatus.isExpensive) { return 10; // Smaller batches on expensive connections } else { return 50; // Default for cellular } } // Priority-based Event Queuing prioritizeEvents() { const events = this.pendingEvents.filter(e => !e.synced); // Sort by timestamp (oldest first) and then by event type (entries before exits) return events.sort((a, b) => { if (a.timestamp !== b.timestamp) { return a.timestamp - b.timestamp; } // Prioritize entry events if (a.type === 'enter' && b.type === 'exit') return -1; if (a.type === 'exit' && b.type === 'enter') return 1; return 0; }); } // Data Compression for Bandwidth Optimization compressEvents(events) { // Simple compression by removing redundant data const compressed = events.map(event => ({ i: event.id, t: event.type === 'enter' ? 1 : 0, r: event.regionId, lat: Math.round(event.location.latitude * 1000000) / 1000000, lng: Math.round(event.location.longitude * 1000000) / 1000000, ts: event.timestamp })); return JSON.stringify(compressed); } decompressEvents(compressed) { const data = JSON.parse(compressed); return data.map((item) => ({ id: item.i, type: item.t === 1 ? 'enter' : 'exit', regionId: item.r, location: { latitude: item.lat, longitude: item.lng, timestamp: item.ts }, timestamp: item.ts, synced: false })); } // Statistics and Monitoring getStatistics() { const syncedEvents = this.pendingEvents.filter(e => e.synced).length; return { totalGeofences: this.geofences.size, activeGeofences: this.getActiveGeofences().length, pendingEvents: this.getPendingEventsCount(), syncedEvents, lastSyncAttempt: this.networkStatus.lastConnected, networkStatus: this.networkStatus }; } // Cleanup and Resource Management cleanup() { // Clear all retry timeouts this.retryTimeouts.forEach(timeout => clearTimeout(timeout)); this.retryTimeouts.clear(); // Clear callbacks this.syncCallbacks.length = 0; // Clear data this.geofences.clear(); this.pendingEvents.length = 0; } } exports.OfflineManager = OfflineManager;