chore: update CHANGELOG.md to include enhancements for dashboard performance, including smarter updates, real-time visitor tracking, and faster event processing
This commit is contained in:
128
lib/hooks/useVisibilityPolling.ts
Normal file
128
lib/hooks/useVisibilityPolling.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
// * Custom hook for visibility-aware polling
|
||||
// * Pauses polling when tab is not visible, resumes when visible
|
||||
// * Reduces server load when users aren't actively viewing the dashboard
|
||||
|
||||
import { useEffect, useRef, useState, useCallback } from 'react'
|
||||
|
||||
interface UseVisibilityPollingOptions {
|
||||
// * Polling interval when tab is visible (in milliseconds)
|
||||
visibleInterval: number
|
||||
// * Polling interval when tab is hidden (in milliseconds, or null to pause)
|
||||
hiddenInterval: number | null
|
||||
}
|
||||
|
||||
interface UseVisibilityPollingReturn {
|
||||
// * Whether polling is currently active
|
||||
isPolling: boolean
|
||||
// * Time since last poll
|
||||
lastPollTime: number | null
|
||||
// * Force a poll immediately
|
||||
triggerPoll: () => void
|
||||
}
|
||||
|
||||
export function useVisibilityPolling(
|
||||
callback: () => void | Promise<void>,
|
||||
options: UseVisibilityPollingOptions
|
||||
): UseVisibilityPollingReturn {
|
||||
const { visibleInterval, hiddenInterval } = options
|
||||
const [isPolling, setIsPolling] = useState(false)
|
||||
const [lastPollTime, setLastPollTime] = useState<number | null>(null)
|
||||
const intervalRef = useRef<NodeJS.Timeout | null>(null)
|
||||
const callbackRef = useRef(callback)
|
||||
|
||||
// * Keep callback reference up to date
|
||||
useEffect(() => {
|
||||
callbackRef.current = callback
|
||||
}, [callback])
|
||||
|
||||
// * Get current polling interval based on visibility
|
||||
const getInterval = useCallback((): number | null => {
|
||||
if (typeof document === 'undefined') return null
|
||||
|
||||
const isVisible = document.visibilityState === 'visible'
|
||||
if (isVisible) {
|
||||
return visibleInterval
|
||||
}
|
||||
return hiddenInterval
|
||||
}, [visibleInterval, hiddenInterval])
|
||||
|
||||
// * Start polling with current interval
|
||||
const startPolling = useCallback(() => {
|
||||
const interval = getInterval()
|
||||
if (interval === null) {
|
||||
setIsPolling(false)
|
||||
return
|
||||
}
|
||||
|
||||
setIsPolling(true)
|
||||
|
||||
// * Clear any existing interval
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
}
|
||||
|
||||
// * Set up new interval
|
||||
intervalRef.current = setInterval(() => {
|
||||
callbackRef.current()
|
||||
setLastPollTime(Date.now())
|
||||
}, interval)
|
||||
}, [getInterval])
|
||||
|
||||
// * Stop polling
|
||||
const stopPolling = useCallback(() => {
|
||||
if (intervalRef.current) {
|
||||
clearInterval(intervalRef.current)
|
||||
intervalRef.current = null
|
||||
}
|
||||
setIsPolling(false)
|
||||
}, [])
|
||||
|
||||
// * Trigger immediate poll
|
||||
const triggerPoll = useCallback(() => {
|
||||
callbackRef.current()
|
||||
setLastPollTime(Date.now())
|
||||
|
||||
// * Restart polling timer
|
||||
startPolling()
|
||||
}, [startPolling])
|
||||
|
||||
// * Handle visibility changes
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') return
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
// * Tab became visible - resume polling with visible interval
|
||||
startPolling()
|
||||
// * Trigger immediate poll to get fresh data
|
||||
triggerPoll()
|
||||
} else {
|
||||
// * Tab hidden - switch to hidden interval or pause
|
||||
const interval = getInterval()
|
||||
if (interval === null) {
|
||||
stopPolling()
|
||||
} else {
|
||||
// * Restart with hidden interval
|
||||
startPolling()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// * Listen for visibility changes
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange)
|
||||
|
||||
// * Start polling initially
|
||||
startPolling()
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
||||
stopPolling()
|
||||
}
|
||||
}, [startPolling, stopPolling, triggerPoll, getInterval])
|
||||
|
||||
return {
|
||||
isPolling,
|
||||
lastPollTime,
|
||||
triggerPoll,
|
||||
}
|
||||
}
|
||||
114
lib/swr/dashboard.ts
Normal file
114
lib/swr/dashboard.ts
Normal file
@@ -0,0 +1,114 @@
|
||||
// * SWR configuration for dashboard data fetching
|
||||
// * Implements stale-while-revalidate pattern for efficient data updates
|
||||
|
||||
import useSWR from 'swr'
|
||||
import { getDashboard, getRealtime, getStats, getDailyStats } from '@/lib/api/stats'
|
||||
import { getSite } from '@/lib/api/sites'
|
||||
import type { Site } from '@/lib/api/sites'
|
||||
import type { Stats, DailyStat } from '@/lib/api/stats'
|
||||
|
||||
// * SWR fetcher functions
|
||||
const fetchers = {
|
||||
site: (siteId: string) => getSite(siteId),
|
||||
dashboard: (siteId: string, start: string, end: string) => getDashboard(siteId, start, end),
|
||||
stats: (siteId: string, start: string, end: string) => getStats(siteId, start, end),
|
||||
dailyStats: (siteId: string, start: string, end: string, interval: 'hour' | 'day' | 'minute') =>
|
||||
getDailyStats(siteId, start, end, interval),
|
||||
realtime: (siteId: string) => getRealtime(siteId),
|
||||
}
|
||||
|
||||
// * Standard SWR config for dashboard data
|
||||
const dashboardSWRConfig = {
|
||||
// * Keep stale data visible while revalidating (better UX)
|
||||
revalidateOnFocus: false,
|
||||
// * Revalidate when reconnecting (fresh data after offline)
|
||||
revalidateOnReconnect: true,
|
||||
// * Retry failed requests
|
||||
shouldRetryOnError: true,
|
||||
errorRetryCount: 3,
|
||||
// * Error retry interval with exponential backoff
|
||||
errorRetryInterval: 5000,
|
||||
}
|
||||
|
||||
// * Hook for site data (loads once, refreshes rarely)
|
||||
export function useSite(siteId: string) {
|
||||
return useSWR<Site>(
|
||||
siteId ? ['site', siteId] : null,
|
||||
() => fetchers.site(siteId),
|
||||
{
|
||||
...dashboardSWRConfig,
|
||||
// * Site data changes rarely, refresh every 5 minutes
|
||||
refreshInterval: 5 * 60 * 1000,
|
||||
// * Deduping interval to prevent duplicate requests
|
||||
dedupingInterval: 30 * 1000,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// * Hook for dashboard summary data (refreshed less frequently)
|
||||
export function useDashboard(siteId: string, start: string, end: string) {
|
||||
return useSWR(
|
||||
siteId && start && end ? ['dashboard', siteId, start, end] : null,
|
||||
() => fetchers.dashboard(siteId, start, end),
|
||||
{
|
||||
...dashboardSWRConfig,
|
||||
// * Refresh every 60 seconds for dashboard summary
|
||||
refreshInterval: 60 * 1000,
|
||||
// * Deduping interval to prevent duplicate requests
|
||||
dedupingInterval: 10 * 1000,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// * Hook for stats (refreshed less frequently)
|
||||
export function useStats(siteId: string, start: string, end: string) {
|
||||
return useSWR<Stats>(
|
||||
siteId && start && end ? ['stats', siteId, start, end] : null,
|
||||
() => fetchers.stats(siteId, start, end),
|
||||
{
|
||||
...dashboardSWRConfig,
|
||||
// * Refresh every 60 seconds for stats
|
||||
refreshInterval: 60 * 1000,
|
||||
dedupingInterval: 10 * 1000,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// * Hook for daily stats (refreshed less frequently)
|
||||
export function useDailyStats(
|
||||
siteId: string,
|
||||
start: string,
|
||||
end: string,
|
||||
interval: 'hour' | 'day' | 'minute'
|
||||
) {
|
||||
return useSWR<DailyStat[]>(
|
||||
siteId && start && end ? ['dailyStats', siteId, start, end, interval] : null,
|
||||
() => fetchers.dailyStats(siteId, start, end, interval),
|
||||
{
|
||||
...dashboardSWRConfig,
|
||||
// * Refresh every 60 seconds for chart data
|
||||
refreshInterval: 60 * 1000,
|
||||
dedupingInterval: 10 * 1000,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// * Hook for realtime visitor count (refreshed frequently)
|
||||
export function useRealtime(siteId: string, refreshInterval: number = 5000) {
|
||||
return useSWR<{ visitors: number }>(
|
||||
siteId ? ['realtime', siteId] : null,
|
||||
() => fetchers.realtime(siteId),
|
||||
{
|
||||
...dashboardSWRConfig,
|
||||
// * Refresh frequently for real-time data (default 5 seconds)
|
||||
refreshInterval,
|
||||
// * Short deduping for real-time
|
||||
dedupingInterval: 2000,
|
||||
// * Keep previous data while loading new data
|
||||
keepPreviousData: true,
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// * Re-export for convenience
|
||||
export { fetchers }
|
||||
Reference in New Issue
Block a user