feat: enhance offline support with improved network management and sync handling

This commit is contained in:
Mo Tarbin
2026-06-06 00:58:18 -04:00
parent 24118e0dc3
commit 4e0b5d79df
9 changed files with 219 additions and 29 deletions

View File

@@ -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)
}

View File

@@ -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)
}