add network listener management and enhance sync on reconnect functionality
This commit is contained in:
@@ -57,6 +57,11 @@ class NetworkManager {
|
||||
registerNetworkListener(callback) {
|
||||
this.connectionStatusListeners.push(callback)
|
||||
}
|
||||
unregisterNetworkListener(callback) {
|
||||
this.connectionStatusListeners = this.connectionStatusListeners.filter(
|
||||
cb => cb !== callback,
|
||||
)
|
||||
}
|
||||
registerBackendSyncListener(callback) {
|
||||
// if callback is not in the list already, add it
|
||||
if (!this.queueSyncListeners.includes(callback)) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
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'
|
||||
@@ -14,6 +16,18 @@ export function useSyncOnReconnect() {
|
||||
const initialized = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
let pendingPollInterval
|
||||
let cacheRefreshInterval
|
||||
let resumeListener
|
||||
let networkListener
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
runSync()
|
||||
}
|
||||
}
|
||||
|
||||
const handleOnline = () => runSync()
|
||||
|
||||
const init = async () => {
|
||||
if (initialized.current) return
|
||||
initialized.current = true
|
||||
@@ -23,24 +37,36 @@ export function useSyncOnReconnect() {
|
||||
}
|
||||
|
||||
// 1. Device network change (works on native + real network drops)
|
||||
networkManager.registerNetworkListener(async isOnline => {
|
||||
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', () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
runSync()
|
||||
}
|
||||
})
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
// 3. Browser online event (fires when device network is restored)
|
||||
window.addEventListener('online', () => runSync())
|
||||
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)
|
||||
setInterval(async () => {
|
||||
pendingPollInterval = setInterval(async () => {
|
||||
const pending = await commandQueue.getPending()
|
||||
if (pending.length > 0) {
|
||||
runSync()
|
||||
@@ -48,7 +74,7 @@ export function useSyncOnReconnect() {
|
||||
}, PENDING_POLL_MS)
|
||||
|
||||
// 5. Keep IDB cache fresh every 5 min while online (so offline reads are current)
|
||||
setInterval(() => {
|
||||
cacheRefreshInterval = setInterval(() => {
|
||||
runSync()
|
||||
}, CACHE_REFRESH_MS)
|
||||
}
|
||||
@@ -62,5 +88,22 @@ export function useSyncOnReconnect() {
|
||||
}
|
||||
|
||||
init()
|
||||
|
||||
return () => {
|
||||
if (pendingPollInterval) {
|
||||
clearInterval(pendingPollInterval)
|
||||
}
|
||||
|
||||
if (cacheRefreshInterval) {
|
||||
clearInterval(cacheRefreshInterval)
|
||||
}
|
||||
|
||||
if (networkListener) {
|
||||
networkManager.unregisterNetworkListener(networkListener)
|
||||
}
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
window.removeEventListener('online', handleOnline)
|
||||
resumeListener?.remove()
|
||||
}
|
||||
}, [queryClient])
|
||||
}
|
||||
|
||||
@@ -150,7 +150,7 @@ export const useCreateChore = () => {
|
||||
return { ...oldData, res: [...oldData.res, offlineChore] }
|
||||
})
|
||||
|
||||
return { res: offlineChore }
|
||||
return offlineChore
|
||||
}
|
||||
|
||||
return useMutation({
|
||||
@@ -170,11 +170,11 @@ export const useCreateChore = () => {
|
||||
}
|
||||
// Successfully created the chore on the server, return the created chore
|
||||
// update the local chores cache with the new chore:
|
||||
queryClient.setQueryData(['chores'], oldData => {
|
||||
queryClient.setQueryData(['chores', false], oldData => {
|
||||
if (!oldData) return { res: [createdChore.res] }
|
||||
return { res: [...oldData.res, createdChore.res] }
|
||||
})
|
||||
return { res: createdChore }
|
||||
return createdChore.res
|
||||
} catch (error) {
|
||||
if (isNetworkError(error)) {
|
||||
return queueOfflineCreate(newTask)
|
||||
|
||||
@@ -36,7 +36,7 @@ export const useCircleMembers = () => {
|
||||
const result = await GetAllCircleMembers()
|
||||
// Cache for offline use
|
||||
if (result?.res) {
|
||||
offlineDB.saveKV('circle_members', result.res)
|
||||
offlineDB.saveKV('circle_members', result.res).catch(() => {})
|
||||
}
|
||||
return result
|
||||
} catch {
|
||||
|
||||
@@ -966,7 +966,6 @@ class OfflineDB {
|
||||
async getHistoryByChore(choreId) {
|
||||
if (!isOfflineFeatureEnabled()) return []
|
||||
await this._ensureInit()
|
||||
console.log('MO: Fetching history for chore', choreId)
|
||||
return this.backend.getHistoryByChore(choreId)
|
||||
}
|
||||
|
||||
|
||||
@@ -367,7 +367,11 @@ const ChoreEdit = () => {
|
||||
|
||||
SaveFunction(chore)
|
||||
.then(result => {
|
||||
if (result?._pendingUpdate || result?._pendingCreate) {
|
||||
if (
|
||||
result?._pendingUpdate ||
|
||||
result?._pendingCreate ||
|
||||
result?.res?._pendingCreate
|
||||
) {
|
||||
showSuccess({
|
||||
title: 'Saved Offline',
|
||||
message: 'Your changes will sync when you are back online.',
|
||||
|
||||
@@ -907,8 +907,8 @@ const ChoreView = () => {
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
const content = decodeHtmlEntities(chore.description || '')
|
||||
const shouldRenderHtml = hasHtmlTags(content)
|
||||
const raw = chore.description || ''
|
||||
const shouldRenderHtml = hasHtmlTags(raw)
|
||||
|
||||
return shouldRenderHtml ? (
|
||||
<Box
|
||||
@@ -916,7 +916,7 @@ const ChoreView = () => {
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
dangerouslySetInnerHTML={{ __html: raw }}
|
||||
/>
|
||||
) : (
|
||||
<Typography
|
||||
@@ -926,7 +926,7 @@ const ChoreView = () => {
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
{decodeHtmlEntities(raw)}
|
||||
</Typography>
|
||||
)
|
||||
})()}
|
||||
@@ -975,8 +975,8 @@ const ChoreView = () => {
|
||||
}}
|
||||
>
|
||||
{(() => {
|
||||
const content = decodeHtmlEntities(chore.notes || '')
|
||||
const shouldRenderHtml = hasHtmlTags(content)
|
||||
const raw = chore.notes || ''
|
||||
const shouldRenderHtml = hasHtmlTags(raw)
|
||||
|
||||
return shouldRenderHtml ? (
|
||||
<Box
|
||||
@@ -984,7 +984,7 @@ const ChoreView = () => {
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
dangerouslySetInnerHTML={{ __html: content }}
|
||||
dangerouslySetInnerHTML={{ __html: raw }}
|
||||
/>
|
||||
) : (
|
||||
<Typography
|
||||
@@ -994,7 +994,7 @@ const ChoreView = () => {
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
{decodeHtmlEntities(raw)}
|
||||
</Typography>
|
||||
)
|
||||
})()}
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import { GetProjects, CreateProject, UpdateProject, DeleteProject } from '../../utils/Fetcher'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
CreateProject,
|
||||
DeleteProject,
|
||||
GetProjects,
|
||||
UpdateProject,
|
||||
} from '../../utils/Fetcher'
|
||||
import { offlineDB } from '../../utils/OfflineDB'
|
||||
|
||||
// Query hook for fetching all projects
|
||||
@@ -33,7 +38,7 @@ export const useCreateProject = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (projectData) => {
|
||||
mutationFn: async projectData => {
|
||||
try {
|
||||
const response = await CreateProject(projectData)
|
||||
if (response.ok) {
|
||||
@@ -53,7 +58,7 @@ export const useCreateProject = () => {
|
||||
return localProject
|
||||
}
|
||||
},
|
||||
onSuccess: (newProject) => {
|
||||
onSuccess: newProject => {
|
||||
// Update the projects cache
|
||||
queryClient.setQueryData(['projects'], (oldProjects = []) => {
|
||||
const updatedProjects = [...oldProjects, newProject]
|
||||
@@ -63,7 +68,7 @@ export const useCreateProject = () => {
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries(['projects'])
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: error => {
|
||||
console.error('Create project mutation failed:', error)
|
||||
},
|
||||
})
|
||||
@@ -92,18 +97,18 @@ export const useUpdateProject = () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
onSuccess: (updatedProject) => {
|
||||
onSuccess: updatedProject => {
|
||||
// Update the projects cache
|
||||
queryClient.setQueryData(['projects'], (oldProjects = []) => {
|
||||
return oldProjects.map(project =>
|
||||
project.id === updatedProject.id ? updatedProject : project
|
||||
project.id === updatedProject.id ? updatedProject : project,
|
||||
)
|
||||
})
|
||||
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries(['projects'])
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: error => {
|
||||
console.error('Update project mutation failed:', error)
|
||||
},
|
||||
})
|
||||
@@ -114,7 +119,7 @@ export const useDeleteProject = () => {
|
||||
const queryClient = useQueryClient()
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (projectId) => {
|
||||
mutationFn: async projectId => {
|
||||
try {
|
||||
// Prevent deletion of default project
|
||||
if (projectId === 'default') {
|
||||
@@ -141,14 +146,14 @@ export const useDeleteProject = () => {
|
||||
// Invalidate and refetch
|
||||
queryClient.invalidateQueries(['projects'])
|
||||
},
|
||||
onError: (error) => {
|
||||
onError: error => {
|
||||
console.error('Delete project mutation failed:', error)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Hook to get a specific project by ID
|
||||
export const useProject = (projectId) => {
|
||||
export const useProject = projectId => {
|
||||
return useQuery({
|
||||
queryKey: ['projects', projectId],
|
||||
queryFn: async () => {
|
||||
|
||||
Reference in New Issue
Block a user