Files
donetick/src/hooks/useSyncOnReconnect.js
2026-08-08 13:40:21 -04:00

139 lines
4.7 KiB
JavaScript

import { App as capacitorApp } from '@capacitor/app'
import { Capacitor } from '@capacitor/core'
import { useQueryClient } from '@tanstack/react-query'
import { useEffect, useRef } from 'react'
import { commandQueue } from '../utils/CommandQueue'
import { offlineDB } from '../utils/OfflineDB'
import { isOAuthExchangeInProgress } from '../utils/OAuthExchangeState'
import { isOfflineFeatureEnabled } from '../utils/OfflineFeatureToggle'
import { syncEngine } from '../utils/SyncEngine'
import { networkManager } from './NetworkManager'
export const PENDING_POLL_MS = 30_000 // retry pending commands every 30s
export const SERVER_PROBE_MS = 15_000 // probe server when marked unreachable but device has network
const CACHE_REFRESH_MS = 5 * 60_000 // refresh IDB cache every 5 min while online
export function useSyncOnReconnect() {
const queryClient = useQueryClient()
const initialized = useRef(false)
useEffect(() => {
let pendingPollInterval
let cacheRefreshInterval
let serverProbeInterval
let resumeListener
let networkListener
const handleVisibilityChange = () => {
if (document.visibilityState === 'visible') {
runSync()
}
}
const handleOnline = () => runSync()
const init = async () => {
if (initialized.current) return
initialized.current = true
if (isOfflineFeatureEnabled()) {
await offlineDB.init()
}
// 1. Device network change (works on native + real network drops)
networkListener = async isOnline => {
if (isOnline) {
await runSync()
}
}
networkManager.registerNetworkListener(networkListener)
// 2. Tab becomes visible (user switches back to the tab after reconnecting backend)
document.addEventListener('visibilitychange', handleVisibilityChange)
// 3. Browser online event (fires when device network is restored)
window.addEventListener('online', handleOnline)
// 3.5 Native app resume (fires when returning to the foreground)
if (Capacitor.isNativePlatform()) {
resumeListener = await capacitorApp.addListener(
'appStateChange',
({ isActive }) => {
if (isActive) {
console.log(
'App resumed, checking connectivity and syncing if online...',
)
runSync()
}
},
)
}
// 4. Retry pending commands every 30s (catches backend restart)
pendingPollInterval = setInterval(async () => {
const pending = await commandQueue.getPending()
if (pending.length > 0) {
runSync()
}
}, PENDING_POLL_MS)
// 5. Keep IDB cache fresh every 5 min while online (so offline reads are current)
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
// Skip while the OAuth code exchange is in flight — there's no session
// yet, so a sync here just 401s. Note the app-resume listener fires in
// the same tick as the deep link, before the route changes, so this has
// to test the shared flag rather than the pathname.
if (isOAuthExchangeInProgress()) return
// No session, nothing to sync — and a 401 here would force a logout that
// hard-navigates signed-out visitors (invite links) away to /login.
if (!localStorage.getItem('token')) 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()
}
}
}
init()
return () => {
if (pendingPollInterval) {
clearInterval(pendingPollInterval)
}
if (cacheRefreshInterval) {
clearInterval(cacheRefreshInterval)
}
if (serverProbeInterval) {
clearInterval(serverProbeInterval)
}
if (networkListener) {
networkManager.unregisterNetworkListener(networkListener)
}
document.removeEventListener('visibilitychange', handleVisibilityChange)
window.removeEventListener('online', handleOnline)
resumeListener?.remove()
}
}, [queryClient])
}