zammad/app/frontend/shared/composables/useQueryPolling.ts
2026-02-26 11:05:44 +01:00

121 lines
3.5 KiB
TypeScript

// Copyright (C) 2012-2026 Zammad Foundation, https://zammad-foundation.org/
import { watchPausable } from '@vueuse/core'
import { ref, type Ref, type ComputedRef, onScopeDispose, toRef, watch, toValue } from 'vue'
import type { QueryHandler } from '#shared/server/apollo/handler'
import { connected } from '#shared/server/connection.ts'
import type { OperationQueryResult } from '#shared/types/server/apollo/handler'
import type { OperationVariables } from '@apollo/client/core'
export type QueryPollingOptions = {
enabled?: boolean // Enable polling, default is true.
randomize?: boolean // Randomize the interval (1000 milliseconds). Useful to prevent request at the same time.
}
const activePollings = new Set<() => void>()
export const stopAllQueryPollings = () => {
activePollings.forEach((stop) => stop())
}
export const useQueryPolling = <
TResult extends OperationQueryResult = OperationQueryResult,
TVariables extends OperationVariables = OperationVariables,
>(
query: QueryHandler<TResult, TVariables>,
interval: number | Ref<number> | ComputedRef<number> | (() => number),
variables?:
| Ref<Partial<TVariables>>
| ComputedRef<Partial<TVariables>>
| (() => Partial<TVariables>),
options?:
| QueryPollingOptions
| Ref<QueryPollingOptions>
| ComputedRef<QueryPollingOptions>
| (() => QueryPollingOptions),
) => {
const isPolling = ref(false)
let pollTimer: ReturnType<typeof setTimeout>
// Only randomize up to +1000ms to avoid requests happening at the same time
const randomizeInterval = toValue(options)?.randomize ? Math.floor(Math.random() * 1000) : 0
const intervalRef = toRef(interval)
const stopPolling = () => {
if (!isPolling.value) return
clearTimeout(pollTimer)
isPolling.value = false
activePollings.delete(stopPolling)
}
const startPolling = () => {
if (isPolling.value || (toValue(options)?.enabled !== undefined && !toValue(options)?.enabled))
return
isPolling.value = true
activePollings.add(stopPolling)
const poll = async () => {
const pollVariables = typeof variables === 'function' ? variables() : variables?.value
await query.refetch(pollVariables as TVariables).catch(() => {})
// Only schedule next poll after current one completes
if (isPolling.value) {
pollTimer = setTimeout(poll, intervalRef.value + randomizeInterval)
}
}
// Schedule first poll after interval instead of running immediately
pollTimer = setTimeout(poll, intervalRef.value + randomizeInterval)
}
watch(
() => toValue(options)?.enabled,
(newValue) => {
if (newValue) {
startPolling()
return
}
stopPolling()
},
)
// When connection is lost or established again, start or stop polling accordingly.
const { resume: startConnectionWatch } = watchPausable(
connected,
(newValue) => {
if (!isPolling.value && toValue(options)?.enabled !== undefined && !toValue(options)?.enabled)
return
if (newValue && !isPolling.value) {
startPolling()
} else if (!newValue && isPolling.value) {
stopPolling()
}
},
{
initialState: 'paused',
},
)
// Start the connection watcher only after the polling was started at least once.
watch(isPolling, startConnectionWatch, {
once: true,
})
// Automatically stop polling when scope is disposed
onScopeDispose(() => {
stopPolling()
})
return {
isPolling,
startPolling,
stopPolling,
}
}