Files
portal/app/resources/js/composables/usePolling.ts
T
Дмитрий 01c20e7b6c phase2(polling): usePolling composable 30 сек + Page Visibility pause
Закрывает последний unblocked production-TODO «Polling/SSE для real-time».
Manual reload-btn остаётся как fast-path; polling — фоновый автообновитель.

Composable (composables/usePolling.ts):
- usePolling(loader, {intervalMs=30_000, enabled=true}).
- Page Visibility API: при document.hidden=true interval останавливается;
  при visibilitychange с возвратом hidden=false — restart + немедленный
  loader() (не ждать следующего interval'а).
- Cleanup на onBeforeUnmount: clearInterval + removeEventListener.
- enabled=false — composable не стартует (feature-flag).

Integration:
- DealsView + KanbanView → loadDeals.
- AdminTenantsView → loadTenants.
- AdminBillingView → loadBilling.
- AdminIncidentsView → loadIncidents.

Vitest +6 (usePolling.spec.ts) с vi.useFakeTimers:
- Вызов каждые intervalMs / default 30 сек / skip при document.hidden /
  cleanup на unmount / enabled=false → no-op / visibilitychange
  pause+resume с немедленным loader.

Регресс:
- Lint+type-check+format passed.
- Vitest 319/319 за 18.67 сек (+6 от 313).
- Vite build 899 ms.
- Pint + PHPStan passed.
- Pest 266/266 за 28.62 сек (backend не тронут).

Реестр v1.71→v1.72 / CLAUDE.md v1.62→v1.63.
ВСЕ unblocked production-TODO закрыты.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 10:17:51 +03:00

73 lines
2.7 KiB
TypeScript

import { onBeforeUnmount, onMounted } from 'vue';
/**
* 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 {
/** Период polling в миллисекундах. По умолчанию 30_000. */
intervalMs?: number;
/** Если false — composable не стартует interval (для disable-флага). */
enabled?: boolean;
}
export function usePolling(loader: () => void | Promise<void>, options: PollingOptions = {}): void {
const intervalMs = options.intervalMs ?? 30_000;
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;
}
});
}