Notifications: fallback de polling + catch-up ao focar a aba
Realtime em ambiente self-hosted às vezes perde eventos (WebSocket desconecta silenciosamente, JWT expira, sleep do SO, etc). Sem fallback, system_alert chega no DB mas toast nunca dispara — usuário só vê ao relogar ou recarregar. Três caminhos complementares agora: 1. Realtime (instantâneo, quando funciona) 2. visibilitychange — ao voltar pro foco da aba, recarrega notificações e dispara toast pras system_alert não-lidas ainda não exibidas 3. Polling a cada 60s como redundância Set alertedIds (in-memory por sessão) evita toast duplicado quando dois caminhos entregam a mesma notif. Seed inicial marca notifs já lidas/ arquivadas no mount pra não disparar retroativamente. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -23,14 +23,20 @@ import { useNotificationStore } from '@/stores/notificationStore';
|
|||||||
// 24h em ms — na prática "sticky" (PrimeVue Toast não aceita Infinity).
|
// 24h em ms — na prática "sticky" (PrimeVue Toast não aceita Infinity).
|
||||||
const STICKY_TOAST_LIFE_MS = 24 * 60 * 60 * 1000;
|
const STICKY_TOAST_LIFE_MS = 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
// Fallback polling — garante catch-up mesmo se Realtime perder eventos.
|
||||||
|
const POLLING_INTERVAL_MS = 60_000;
|
||||||
|
|
||||||
export function useNotifications() {
|
export function useNotifications() {
|
||||||
const store = useNotificationStore();
|
const store = useNotificationStore();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
const alertedIds = new Set(); // ids que já dispararam toast nesta sessão
|
||||||
|
let ownerId = null;
|
||||||
|
let pollTimer = null;
|
||||||
|
|
||||||
function onRealtimeNotification(item) {
|
function showToastFor(item) {
|
||||||
// Toast aparece pra alertas de sistema (heartbeat, infra, etc).
|
|
||||||
// Inbound/agendas têm seus próprios notifiers visuais.
|
|
||||||
if (item?.type !== 'system_alert') return;
|
if (item?.type !== 'system_alert') return;
|
||||||
|
if (alertedIds.has(item.id)) return;
|
||||||
|
alertedIds.add(item.id);
|
||||||
const payload = item.payload || {};
|
const payload = item.payload || {};
|
||||||
toast.add({
|
toast.add({
|
||||||
severity: 'error',
|
severity: 'error',
|
||||||
@@ -41,16 +47,53 @@ export function useNotifications() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onRealtimeNotification(item) {
|
||||||
|
showToastFor(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-carrega notifs do DB e dispara toast pras system_alert não-lidas
|
||||||
|
// que ainda não foram vistas nesta sessão. Usado no mount, no visibilitychange
|
||||||
|
// e no polling de fallback pro caso de Realtime perder eventos.
|
||||||
|
async function refreshAndMaybeAlert() {
|
||||||
|
if (!ownerId) return;
|
||||||
|
await store.load(ownerId);
|
||||||
|
for (const item of store.items || []) {
|
||||||
|
if (item.type !== 'system_alert') continue;
|
||||||
|
if (item.read_at || item.archived) continue;
|
||||||
|
showToastFor(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onVisibilityChange() {
|
||||||
|
if (document.visibilityState === 'visible') {
|
||||||
|
refreshAndMaybeAlert();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const { data, error } = await supabase.auth.getUser();
|
const { data, error } = await supabase.auth.getUser();
|
||||||
if (error || !data?.user?.id) return;
|
if (error || !data?.user?.id) return;
|
||||||
|
|
||||||
const ownerId = data.user.id;
|
ownerId = data.user.id;
|
||||||
await store.load(ownerId);
|
await store.load(ownerId);
|
||||||
|
|
||||||
|
// Seed do set: notifs system_alert já lidas/vistas não redisparam toast
|
||||||
|
for (const item of store.items || []) {
|
||||||
|
if (item.type === 'system_alert' && (item.read_at || item.archived)) {
|
||||||
|
alertedIds.add(item.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
store.subscribeRealtime(ownerId, onRealtimeNotification);
|
store.subscribeRealtime(ownerId, onRealtimeNotification);
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', onVisibilityChange);
|
||||||
|
pollTimer = setInterval(refreshAndMaybeAlert, POLLING_INTERVAL_MS);
|
||||||
});
|
});
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('visibilitychange', onVisibilityChange);
|
||||||
|
if (pollTimer) clearInterval(pollTimer);
|
||||||
|
pollTimer = null;
|
||||||
store.unsubscribe();
|
store.unsubscribe();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user