mirror of
https://github.com/DocNR/POWR.git
synced 2025-06-05 16:52:07 +00:00
359 lines
10 KiB
TypeScript
359 lines
10 KiB
TypeScript
// lib/services/ConnectivityService.ts
|
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import NetInfo, { NetInfoState } from '@react-native-community/netinfo';
|
|
import { openDatabaseSync } from 'expo-sqlite';
|
|
|
|
/**
|
|
* Service to monitor network connectivity and provide status information
|
|
*/
|
|
export class ConnectivityService {
|
|
private static instance: ConnectivityService;
|
|
private isOnline: boolean = false;
|
|
private lastOnlineTime: number | null = null;
|
|
private listeners: Set<(isOnline: boolean) => void> = new Set();
|
|
private syncListeners: Set<() => void> = new Set();
|
|
private checkingStatus: boolean = false;
|
|
private offlineMode: boolean = false;
|
|
|
|
// Singleton pattern
|
|
static getInstance(): ConnectivityService {
|
|
if (!ConnectivityService.instance) {
|
|
ConnectivityService.instance = new ConnectivityService();
|
|
}
|
|
return ConnectivityService.instance;
|
|
}
|
|
|
|
private constructor() {
|
|
// Initialize network monitoring
|
|
this.setupNetworkMonitoring();
|
|
}
|
|
|
|
/**
|
|
* Setup network state change monitoring
|
|
*/
|
|
private setupNetworkMonitoring(): void {
|
|
// Subscribe to network state updates
|
|
NetInfo.addEventListener(this.handleNetworkChange);
|
|
|
|
// Initial network check
|
|
this.checkNetworkStatus();
|
|
}
|
|
|
|
/**
|
|
* Handle network state changes
|
|
*/
|
|
private handleNetworkChange = (state: NetInfoState): void => {
|
|
// Skip if in forced offline mode
|
|
if (this.offlineMode) {
|
|
return;
|
|
}
|
|
|
|
const previousStatus = this.isOnline;
|
|
const newOnlineStatus = state.isConnected === true && state.isInternetReachable !== false;
|
|
|
|
// Only trigger updates if status actually changed
|
|
if (this.isOnline !== newOnlineStatus) {
|
|
this.isOnline = newOnlineStatus;
|
|
|
|
// Update last online time if we're going online
|
|
if (newOnlineStatus) {
|
|
this.lastOnlineTime = Date.now();
|
|
}
|
|
|
|
this.updateStatusInDatabase(newOnlineStatus);
|
|
this.notifyListeners();
|
|
|
|
// If we're coming back online, trigger sync
|
|
if (newOnlineStatus && !previousStatus) {
|
|
console.log('[ConnectivityService] Network connection restored, triggering sync');
|
|
this.triggerSync();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Perform a network status check
|
|
* This can be called manually to force a check
|
|
*/
|
|
async checkNetworkStatus(): Promise<boolean> {
|
|
// Skip if already checking
|
|
if (this.checkingStatus) {
|
|
return this.isOnline;
|
|
}
|
|
|
|
// Skip if in forced offline mode
|
|
if (this.offlineMode) {
|
|
return false;
|
|
}
|
|
|
|
try {
|
|
this.checkingStatus = true;
|
|
|
|
// First get the network state from NetInfo
|
|
const state = await NetInfo.fetch();
|
|
|
|
// Perform a more thorough check if NetInfo says we're connected
|
|
let isReachable = state.isConnected === true && state.isInternetReachable !== false;
|
|
|
|
// If NetInfo says we're connected, do an additional check with a fetch request
|
|
if (isReachable) {
|
|
try {
|
|
// Try to fetch a small resource with a timeout
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
|
|
// Use a reliable endpoint that should always be available
|
|
const response = await fetch('https://www.google.com/generate_204', {
|
|
method: 'HEAD',
|
|
signal: controller.signal,
|
|
cache: 'no-cache',
|
|
});
|
|
|
|
clearTimeout(timeoutId);
|
|
|
|
// If we get a response, we're definitely online
|
|
isReachable = response.status === 204 || response.ok;
|
|
} catch (fetchError) {
|
|
// If the fetch fails, we might not have real connectivity
|
|
console.log('[ConnectivityService] Fetch check failed:', fetchError);
|
|
isReachable = false;
|
|
}
|
|
}
|
|
|
|
const previousStatus = this.isOnline;
|
|
this.isOnline = isReachable;
|
|
|
|
// Update last online time if we're online
|
|
if (this.isOnline) {
|
|
this.lastOnlineTime = Date.now();
|
|
}
|
|
|
|
// Update database and notify if status changed
|
|
if (previousStatus !== this.isOnline) {
|
|
this.updateStatusInDatabase(this.isOnline);
|
|
this.notifyListeners();
|
|
|
|
// If we're coming back online, trigger sync
|
|
if (this.isOnline && !previousStatus) {
|
|
console.log('[ConnectivityService] Network connection restored, triggering sync');
|
|
this.triggerSync();
|
|
}
|
|
}
|
|
|
|
return this.isOnline;
|
|
} catch (error) {
|
|
console.error('[ConnectivityService] Error checking network status:', error);
|
|
this.isOnline = false;
|
|
return false;
|
|
} finally {
|
|
this.checkingStatus = false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Set forced offline mode (for testing or battery saving)
|
|
*/
|
|
setOfflineMode(enabled: boolean): void {
|
|
this.offlineMode = enabled;
|
|
|
|
if (enabled) {
|
|
// Force offline status
|
|
const previousStatus = this.isOnline;
|
|
this.isOnline = false;
|
|
|
|
// Update database and notify if status changed
|
|
if (previousStatus) {
|
|
this.updateStatusInDatabase(false);
|
|
this.notifyListeners();
|
|
}
|
|
} else {
|
|
// Re-check network status when disabling offline mode
|
|
this.checkNetworkStatus();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Update online status in the database
|
|
*/
|
|
private async updateStatusInDatabase(isOnline: boolean): Promise<void> {
|
|
try {
|
|
const db = openDatabaseSync('powr.db');
|
|
|
|
// Create the app_status table if it doesn't exist
|
|
await db.runAsync(`
|
|
CREATE TABLE IF NOT EXISTS app_status (
|
|
key TEXT PRIMARY KEY,
|
|
value TEXT,
|
|
updated_at INTEGER
|
|
)
|
|
`);
|
|
|
|
await db.runAsync(
|
|
`INSERT OR REPLACE INTO app_status (key, value, updated_at)
|
|
VALUES (?, ?, ?)`,
|
|
['online_status', isOnline ? 'online' : 'offline', Date.now()]
|
|
);
|
|
|
|
// Also store last online time if we're online
|
|
if (isOnline && this.lastOnlineTime) {
|
|
await db.runAsync(
|
|
`INSERT OR REPLACE INTO app_status (key, value, updated_at)
|
|
VALUES (?, ?, ?)`,
|
|
['last_online_time', this.lastOnlineTime.toString(), Date.now()]
|
|
);
|
|
}
|
|
} catch (error) {
|
|
console.error('[ConnectivityService] Error updating status in database:', error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Notify all registered listeners of connectivity change
|
|
*/
|
|
private notifyListeners(): void {
|
|
this.listeners.forEach(listener => {
|
|
try {
|
|
listener(this.isOnline);
|
|
} catch (error) {
|
|
console.error('[ConnectivityService] Error in listener:', error);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Trigger sync operations when coming back online
|
|
*/
|
|
private triggerSync(): void {
|
|
this.syncListeners.forEach(listener => {
|
|
try {
|
|
listener();
|
|
} catch (error) {
|
|
console.error('[ConnectivityService] Error in sync listener:', error);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get current network connectivity status
|
|
*/
|
|
getConnectionStatus(): boolean {
|
|
return this.isOnline;
|
|
}
|
|
|
|
/**
|
|
* Get the last time the device was online
|
|
*/
|
|
getLastOnlineTime(): number | null {
|
|
return this.lastOnlineTime;
|
|
}
|
|
|
|
/**
|
|
* Register a listener for connectivity changes
|
|
*/
|
|
addListener(listener: (isOnline: boolean) => void): () => void {
|
|
this.listeners.add(listener);
|
|
|
|
// Return function to remove the listener
|
|
return () => {
|
|
this.listeners.delete(listener);
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Register a sync listener that will be called when connectivity is restored
|
|
*/
|
|
addSyncListener(listener: () => void): () => void {
|
|
this.syncListeners.add(listener);
|
|
return () => this.syncListeners.delete(listener);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* React hook for using connectivity status in components
|
|
*/
|
|
export function useConnectivity() {
|
|
const [isOnline, setIsOnline] = useState<boolean>(() => {
|
|
// Initialize with current status
|
|
return ConnectivityService.getInstance().getConnectionStatus();
|
|
});
|
|
|
|
const [lastOnlineTime, setLastOnlineTime] = useState<number | null>(() => {
|
|
// Initialize with last online time
|
|
return ConnectivityService.getInstance().getLastOnlineTime();
|
|
});
|
|
|
|
// Use a ref to track if we're currently checking connectivity
|
|
const isCheckingRef = useRef(false);
|
|
|
|
useEffect(() => {
|
|
// Register listener for updates
|
|
const removeListener = ConnectivityService.getInstance().addListener((online) => {
|
|
setIsOnline(online);
|
|
if (online) {
|
|
setLastOnlineTime(Date.now());
|
|
}
|
|
});
|
|
|
|
// Perform an initial check when the component mounts
|
|
if (!isCheckingRef.current) {
|
|
isCheckingRef.current = true;
|
|
ConnectivityService.getInstance().checkNetworkStatus()
|
|
.then(online => {
|
|
setIsOnline(online);
|
|
if (online) {
|
|
setLastOnlineTime(Date.now());
|
|
}
|
|
})
|
|
.finally(() => {
|
|
isCheckingRef.current = false;
|
|
});
|
|
}
|
|
|
|
// Set up periodic checks while the component is mounted
|
|
const intervalId = setInterval(() => {
|
|
if (!isCheckingRef.current) {
|
|
isCheckingRef.current = true;
|
|
ConnectivityService.getInstance().checkNetworkStatus()
|
|
.then(online => {
|
|
setIsOnline(online);
|
|
if (online) {
|
|
setLastOnlineTime(Date.now());
|
|
}
|
|
})
|
|
.finally(() => {
|
|
isCheckingRef.current = false;
|
|
});
|
|
}
|
|
}, 30000); // Check every 30 seconds
|
|
|
|
// Clean up on unmount
|
|
return () => {
|
|
removeListener();
|
|
clearInterval(intervalId);
|
|
};
|
|
}, []);
|
|
|
|
// Function to manually check network status
|
|
const checkConnection = useCallback(async (): Promise<boolean> => {
|
|
if (isCheckingRef.current) return isOnline;
|
|
|
|
isCheckingRef.current = true;
|
|
try {
|
|
const online = await ConnectivityService.getInstance().checkNetworkStatus();
|
|
setIsOnline(online);
|
|
if (online) {
|
|
setLastOnlineTime(Date.now());
|
|
}
|
|
return online;
|
|
} finally {
|
|
isCheckingRef.current = false;
|
|
}
|
|
}, [isOnline]);
|
|
|
|
return {
|
|
isOnline,
|
|
lastOnlineTime,
|
|
checkConnection
|
|
};
|
|
}
|