Files
2026-03-23 21:43:40 +00:00

200 lines
4.5 KiB
TypeScript

import { ref, onMounted, onUnmounted } from 'vue'
export interface PollingOptions {
interval?: number // milliseconds
immediate?: boolean // whether to execute immediately on start
maxRetries?: number // maximum number of retries on error
retryDelay?: number // delay between retries in milliseconds
onError?: (error: Error) => void // error handler
}
export interface PollingState {
isPolling: boolean
isFetching: boolean
error: string | null
retryCount: number
lastFetchTime: Date | null
}
/**
* Composable for implementing polling/real-time updates
*
* @param callback - Function to execute on each poll
* @param options - Polling configuration options
* @returns Polling controls and state
*/
export const usePolling = <T>(
callback: () => Promise<T> | T,
options: PollingOptions = {}
) => {
const {
interval = 3000, // 3 seconds default
immediate = true,
maxRetries = 3,
retryDelay = 1000,
onError
} = options
// State
const state = ref<PollingState>({
isPolling: false,
isFetching: false,
error: null,
retryCount: 0,
lastFetchTime: null
})
// Polling interval reference
let pollInterval: NodeJS.Timeout | null = null
let retryTimeout: NodeJS.Timeout | null = null
// Execute the polling callback
const executePoll = async (): Promise<T | null> => {
if (state.value.isFetching) {
return null // Skip if already fetching
}
state.value.isFetching = true
state.value.error = null
try {
const result = await callback()
state.value.lastFetchTime = new Date()
state.value.retryCount = 0 // Reset retry count on success
return result
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
state.value.error = errorMessage
state.value.retryCount++
// Call error handler if provided
if (onError) {
onError(error instanceof Error ? error : new Error(errorMessage))
}
// Handle retries
if (state.value.retryCount <= maxRetries) {
console.warn(`Polling error (retry ${state.value.retryCount}/${maxRetries}):`, errorMessage)
// Schedule retry
if (retryTimeout) {
clearTimeout(retryTimeout)
}
retryTimeout = setTimeout(() => {
executePoll()
}, retryDelay)
} else {
console.error(`Polling failed after ${maxRetries} retries:`, errorMessage)
stopPolling() // Stop polling after max retries
}
return null
} finally {
state.value.isFetching = false
}
}
// Start polling
const startPolling = () => {
if (state.value.isPolling) {
return // Already polling
}
state.value.isPolling = true
state.value.error = null
// Execute immediately if requested
if (immediate) {
executePoll()
}
// Set up interval
pollInterval = setInterval(() => {
executePoll()
}, interval)
}
// Stop polling
const stopPolling = () => {
if (pollInterval) {
clearInterval(pollInterval)
pollInterval = null
}
if (retryTimeout) {
clearTimeout(retryTimeout)
retryTimeout = null
}
state.value.isPolling = false
state.value.isFetching = false
}
// Toggle polling
const togglePolling = () => {
if (state.value.isPolling) {
stopPolling()
} else {
startPolling()
}
}
// Force immediate execution
const forcePoll = async (): Promise<T | null> => {
return await executePoll()
}
// Update polling interval
const updateInterval = (newInterval: number) => {
const wasPolling = state.value.isPolling
if (wasPolling) {
stopPolling()
}
// Update interval in options (for next start)
options.interval = newInterval
if (wasPolling) {
startPolling()
}
}
// Cleanup on unmount
onUnmounted(() => {
stopPolling()
})
// Auto-start on mount if immediate is true
onMounted(() => {
if (immediate) {
startPolling()
}
})
return {
// State
state: state.value,
isPolling: state.value.isPolling,
isFetching: state.value.isFetching,
error: state.value.error,
retryCount: state.value.retryCount,
lastFetchTime: state.value.lastFetchTime,
// Controls
startPolling,
stopPolling,
togglePolling,
forcePoll,
updateInterval,
// Helper
resetError: () => {
state.value.error = null
state.value.retryCount = 0
}
}
}
export default usePolling