feat: enhance offline support with improved network management and sync handling
This commit is contained in:
@@ -1,47 +1,83 @@
|
||||
import { Network } from '@capacitor/network'
|
||||
import { isOfflineFeatureEnabled } from '../utils/OfflineFeatureToggle'
|
||||
|
||||
class NetworkManager {
|
||||
constructor() {
|
||||
this.isOnline = true
|
||||
this.isNetworkOn = null
|
||||
this.init()
|
||||
this.deviceOnline = true
|
||||
this.serverReachable = true
|
||||
this.offlineReason = null // 'device' | 'server' | null
|
||||
this.connectionStatusListeners = []
|
||||
this.queueSyncListeners = []
|
||||
this.lastChecked = null
|
||||
this.offlineSince = null
|
||||
this.init()
|
||||
}
|
||||
|
||||
// Effective online status: both device network AND server must be reachable
|
||||
get isOnline() {
|
||||
return this.deviceOnline && this.serverReachable
|
||||
}
|
||||
|
||||
// Alias for backward compatibility (DeveloperSettings uses this)
|
||||
get isNetworkOn() {
|
||||
return this.deviceOnline
|
||||
}
|
||||
|
||||
async init() {
|
||||
const status = await Network.getStatus()
|
||||
this.isNetworkOn = status.connected
|
||||
this.deviceOnline = status.connected
|
||||
this.lastChecked = Date.now()
|
||||
if (!status.connected) {
|
||||
this.offlineReason = 'device'
|
||||
this.offlineSince = Date.now()
|
||||
}
|
||||
|
||||
Network.addListener('networkStatusChange', status => {
|
||||
if (this.isNetworkOn !== status.connected) {
|
||||
this.isNetworkOn = status.connected
|
||||
if (this.deviceOnline !== status.connected) {
|
||||
this.deviceOnline = status.connected
|
||||
this.lastChecked = Date.now()
|
||||
this.isOnline = status.connected
|
||||
|
||||
if (!status.connected) {
|
||||
this.offlineReason = 'device'
|
||||
this.offlineSince = Date.now()
|
||||
} else {
|
||||
// Device came back online — update reason based on server state
|
||||
this.offlineReason = this.serverReachable ? null : 'server'
|
||||
}
|
||||
this.notifyConnectionStatus()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
setOffline() {
|
||||
if (this.isOnline === true) {
|
||||
this.isOnline = false
|
||||
// Called when a fetch() response is received (any HTTP status = server is up)
|
||||
setServerReachable() {
|
||||
if (!this.serverReachable) {
|
||||
this.serverReachable = true
|
||||
this.offlineReason = this.deviceOnline ? null : 'device'
|
||||
this.notifyConnectionStatus()
|
||||
this.offlineSince = Date.now() // Record the time when we went offline
|
||||
}
|
||||
}
|
||||
setOnline() {
|
||||
if (this.isOnline === false) {
|
||||
this.isOnline = true
|
||||
|
||||
// Called when fetch() throws a network error (server unreachable)
|
||||
// Only takes effect when offline mode is enabled
|
||||
setServerUnreachable() {
|
||||
if (!isOfflineFeatureEnabled()) return
|
||||
if (this.serverReachable) {
|
||||
this.serverReachable = false
|
||||
this.offlineReason = 'server'
|
||||
this.offlineSince = Date.now()
|
||||
this.notifyConnectionStatus()
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy methods kept for compatibility
|
||||
setOffline() {
|
||||
this.setServerUnreachable()
|
||||
}
|
||||
setOnline() {
|
||||
this.setServerReachable()
|
||||
}
|
||||
|
||||
notifyConnectionStatus() {
|
||||
this.connectionStatusListeners.forEach(callback => {
|
||||
callback(this.isOnline)
|
||||
@@ -63,7 +99,6 @@ class NetworkManager {
|
||||
)
|
||||
}
|
||||
registerBackendSyncListener(callback) {
|
||||
// if callback is not in the list already, add it
|
||||
if (!this.queueSyncListeners.includes(callback)) {
|
||||
this.queueSyncListeners.push(callback)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { networkManager } from './NetworkManager'
|
||||
|
||||
const PENDING_POLL_MS = 30_000 // retry pending commands every 30s
|
||||
const CACHE_REFRESH_MS = 5 * 60_000 // refresh IDB cache every 5 min while online
|
||||
const SERVER_PROBE_MS = 15_000 // probe server when marked unreachable but device has network
|
||||
|
||||
export function useSyncOnReconnect() {
|
||||
const queryClient = useQueryClient()
|
||||
@@ -18,6 +19,7 @@ export function useSyncOnReconnect() {
|
||||
useEffect(() => {
|
||||
let pendingPollInterval
|
||||
let cacheRefreshInterval
|
||||
let serverProbeInterval
|
||||
let resumeListener
|
||||
let networkListener
|
||||
const handleVisibilityChange = () => {
|
||||
@@ -77,13 +79,27 @@ export function useSyncOnReconnect() {
|
||||
cacheRefreshInterval = setInterval(() => {
|
||||
runSync()
|
||||
}, CACHE_REFRESH_MS)
|
||||
|
||||
// 6. Probe server every 15s when server is unreachable but device has network
|
||||
serverProbeInterval = setInterval(async () => {
|
||||
if (!networkManager.isOnline && networkManager.deviceOnline) {
|
||||
await runSync()
|
||||
}
|
||||
}, SERVER_PROBE_MS)
|
||||
}
|
||||
|
||||
const runSync = async () => {
|
||||
if (!isOfflineFeatureEnabled()) return
|
||||
const wasOffline = !networkManager.isOnline
|
||||
const didSync = await syncEngine.sync()
|
||||
if (didSync) {
|
||||
queryClient.invalidateQueries()
|
||||
// After recovery from server-unreachable, run a second pass to flush
|
||||
// any commands that were skipped while offline
|
||||
if (wasOffline && networkManager.isOnline) {
|
||||
const didSync2 = await syncEngine.sync()
|
||||
if (didSync2) queryClient.invalidateQueries()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,6 +114,10 @@ export function useSyncOnReconnect() {
|
||||
clearInterval(cacheRefreshInterval)
|
||||
}
|
||||
|
||||
if (serverProbeInterval) {
|
||||
clearInterval(serverProbeInterval)
|
||||
}
|
||||
|
||||
if (networkListener) {
|
||||
networkManager.unregisterNetworkListener(networkListener)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user