2026-05-09 10:17:51 +03:00
|
|
|
|
import { onBeforeUnmount, onMounted } from 'vue';
|
2026-05-17 07:54:39 +03:00
|
|
|
|
import { POLLING_INTERVAL_MS } from '../constants/polling';
|
2026-05-09 10:17:51 +03:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Polling-composable для авто-обновления view-данных.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Используется в DealsView/KanbanView/AdminTenants/Billing/Incidents как
|
|
|
|
|
|
* замена manual reload-btn — каждые `intervalMs` миллисекунд вызывает
|
|
|
|
|
|
* переданный `loader()`. На MVP это покрывает «real-time» pattern до
|
|
|
|
|
|
* приезда SSE/WebSocket в production.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Pause при скрытой вкладке (`document.hidden=true`) через Page Visibility
|
|
|
|
|
|
* API — не делаем лишних запросов когда пользователь смотрит другую
|
|
|
|
|
|
* вкладку. Resume на visibilitychange-event.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Cleanup на onBeforeUnmount: clearInterval + removeEventListener.
|
|
|
|
|
|
*/
|
|
|
|
|
|
export interface PollingOptions {
|
2026-05-17 07:54:39 +03:00
|
|
|
|
/** Период polling в миллисекундах. По умолчанию POLLING_INTERVAL_MS (30 с). */
|
2026-05-09 10:17:51 +03:00
|
|
|
|
intervalMs?: number;
|
|
|
|
|
|
/** Если false — composable не стартует interval (для disable-флага). */
|
|
|
|
|
|
enabled?: boolean;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export function usePolling(loader: () => void | Promise<void>, options: PollingOptions = {}): void {
|
2026-05-17 07:54:39 +03:00
|
|
|
|
const intervalMs = options.intervalMs ?? POLLING_INTERVAL_MS;
|
2026-05-09 10:17:51 +03:00
|
|
|
|
const enabled = options.enabled ?? true;
|
|
|
|
|
|
|
|
|
|
|
|
if (!enabled) return;
|
|
|
|
|
|
|
|
|
|
|
|
let timerId: ReturnType<typeof setInterval> | null = null;
|
|
|
|
|
|
let visibilityListener: (() => void) | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
function start() {
|
|
|
|
|
|
if (timerId !== null) return;
|
|
|
|
|
|
timerId = setInterval(() => {
|
|
|
|
|
|
// Skip если вкладка скрыта — экономим запросы.
|
|
|
|
|
|
if (typeof document !== 'undefined' && document.hidden) return;
|
|
|
|
|
|
void loader();
|
|
|
|
|
|
}, intervalMs);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function stop() {
|
|
|
|
|
|
if (timerId !== null) {
|
|
|
|
|
|
clearInterval(timerId);
|
|
|
|
|
|
timerId = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
start();
|
|
|
|
|
|
if (typeof document !== 'undefined') {
|
|
|
|
|
|
visibilityListener = () => {
|
|
|
|
|
|
if (document.hidden) {
|
|
|
|
|
|
stop();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
start();
|
|
|
|
|
|
// Сразу подгрузить свежее на возврат во вкладку (не ждать interval).
|
|
|
|
|
|
void loader();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
document.addEventListener('visibilitychange', visibilityListener);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
onBeforeUnmount(() => {
|
|
|
|
|
|
stop();
|
|
|
|
|
|
if (visibilityListener !== null && typeof document !== 'undefined') {
|
|
|
|
|
|
document.removeEventListener('visibilitychange', visibilityListener);
|
|
|
|
|
|
visibilityListener = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|