VetNet-LCPS / vetcopilot-app /src /hooks /useOfflineCache.ts
thomasm291678
VetCopilot-LCPS: LCPS 2.0 本地推理引擎替换 DeepSeek API
793e953
Raw History Blame Contribute Delete
3.78 kB
/**
* useOfflineCache Hook
*
* React hook providing a reactive interface to the IndexedDB-backed
* offline SOAP consultation cache.
*
* Exposes the full cached consultation list, individual lookup,
* cache/remove operations, and online/offline status.
*/
import { useState, useEffect, useCallback, useRef } from 'react'
import type { CachedConsultation } from '../utils/offlineCache'
import * as offlineCache from '../utils/offlineCache'
// ================================================================
// Hook return type
// ================================================================
export interface UseOfflineCacheReturn {
/** All cached consultations, newest first */
cachedConsultations: CachedConsultation[]
/** True while the initial load from IndexedDB is in progress */
isLoading: boolean
/** Cache a consultation record (also refreshes the list) */
cacheConsultation: (data: CachedConsultation) => Promise<void>
/** Look up a single cached consultation by ID */
getConsultation: (id: string) => Promise<CachedConsultation | null>
/** Remove a single cached consultation by ID */
removeConsultation: (id: string) => Promise<void>
/** Whether the browser is currently offline */
isOffline: boolean
}
// ================================================================
// Hook implementation
// ================================================================
export function useOfflineCache(): UseOfflineCacheReturn {
const [cachedConsultations, setCachedConsultations] = useState<CachedConsultation[]>([])
const [isLoading, setIsLoading] = useState(true)
const [isOffline, setIsOffline] = useState(!navigator.onLine)
// Track mount state to prevent state updates after unmount
const mountedRef = useRef(true)
// ----- Load all cached consultations on mount -----
const refreshList = useCallback(async () => {
try {
const items = await offlineCache.getAllCachedConsultations()
if (mountedRef.current) {
setCachedConsultations(items)
}
} catch {
// Silently ignore — cache reads failing shouldn't break the UI
}
}, [])
useEffect(() => {
mountedRef.current = true
const load = async () => {
setIsLoading(true)
await refreshList()
if (mountedRef.current) {
setIsLoading(false)
}
}
load()
return () => {
mountedRef.current = false
}
}, [refreshList])
// ----- Online/offline detection -----
useEffect(() => {
const handleOnline = () => {
if (mountedRef.current) {
setIsOffline(false)
}
}
const handleOffline = () => {
if (mountedRef.current) {
setIsOffline(true)
}
}
window.addEventListener('online', handleOnline)
window.addEventListener('offline', handleOffline)
return () => {
window.removeEventListener('online', handleOnline)
window.removeEventListener('offline', handleOffline)
}
}, [])
// ----- Cache a consultation -----
const cacheConsultation = useCallback(
async (data: CachedConsultation) => {
await offlineCache.cacheConsultation(data)
await refreshList()
},
[refreshList],
)
// ----- Look up a single consultation -----
const getConsultation = useCallback(
async (id: string): Promise<CachedConsultation | null> => {
return offlineCache.getCachedConsultation(id)
},
[],
)
// ----- Remove a single consultation -----
const removeConsultation = useCallback(
async (id: string) => {
await offlineCache.removeConsultation(id)
await refreshList()
},
[refreshList],
)
return {
cachedConsultations,
isLoading,
cacheConsultation,
getConsultation,
removeConsultation,
isOffline,
}
}