/
voting
/
frontend
Обзор
Документация
Войти
/
voting
/
frontend
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
main
src/hooks/useUserFlow.ts
130 строк
4 KB
TimPulin
039-update-sse-unified-payload
19 июл 2026, 15:39
19 июл 2026, 15:39
ce4a214
Код
Авторство
О чём код?
import { useCallback, useEffect, useRef } from 'react' import { votingService } from '../api/voting.service' import { useUserFlowStore } from '../stores/userFlowStore' import type { SseScreenType } from '../types/user-flow' import { mapSseScreenTypeToResultScreenType, parseUserFlowSseEnvelope, type RouterScreen, resolveRouterScreen } from '../utils/userFlowHelpers' import { useSSE } from './useSSE' /** * SSE stream URL relative to canonical VITE_API_URL (…/api/v1). * VITE_API_URL=http://localhost:3000/api/v1 → …/api/v1/events/stream */ export function buildSseUrl(baseUrl: string = import.meta.env.VITE_API_URL as string): string { return `${baseUrl.replace(/\/$/, '')}/events/stream` } export interface UseUserFlowReturn { currentScreen: SseScreenType | null snapshot: ReturnType<typeof useUserFlowStore.getState>['snapshot'] isHydrating: boolean hydrateError: string | null sseStatus: ReturnType<typeof useSSE>['status'] routerScreen: RouterScreen setCurrentScreen: (screen: SseScreenType) => void } /** * Participant user-flow: REST hydrate + SSE-driven screen switching. * Server wins on the next SSE/REST update after manual setCurrentScreen(). */ export function useUserFlow(eventId: string): UseUserFlowReturn { const store = useUserFlowStore() const { status, subscribe } = useSSE(buildSseUrl(), { autoConnect: Boolean(eventId) }) const prevStatusRef = useRef(status) const hydrateGenerationRef = useRef(0) const skipNextConnectedHydrateRef = useRef(true) const hydrate = useCallback(async () => { if (!eventId) return const generation = ++hydrateGenerationRef.current useUserFlowStore.getState().setIsHydrating(true) useUserFlowStore.getState().setHydrateError(null) try { const snapshot = await votingService.getCurrentNomination(eventId) if (generation !== hydrateGenerationRef.current) return useUserFlowStore.getState().applyHydratedSnapshot(snapshot) } catch (err) { if (generation !== hydrateGenerationRef.current) return useUserFlowStore .getState() .setHydrateError(err instanceof Error ? err.message : 'Hydrate failed') // Keep last known snapshot / currentScreen } finally { if (generation === hydrateGenerationRef.current) { useUserFlowStore.getState().setIsHydrating(false) } } }, [eventId]) // Reset + initial hydrate when eventId changes useEffect(() => { useUserFlowStore.getState().reset() useUserFlowStore.getState().setEventId(eventId) skipNextConnectedHydrateRef.current = true void hydrate() return () => { hydrateGenerationRef.current += 1 } }, [eventId, hydrate]) // Re-hydrate when SSE recovers to connected after drop (skip first connect) useEffect(() => { const prev = prevStatusRef.current prevStatusRef.current = status if (status !== 'connected' || prev === 'connected') return if (skipNextConnectedHydrateRef.current) { skipNextConnectedHydrateRef.current = false return } void hydrate() }, [status, hydrate]) // SSE message → parse envelope → update screen (+ hydrate on nominationChanged) useEffect(() => { return subscribe('message', (raw) => { const parsed = parseUserFlowSseEnvelope(raw) if (!parsed) return if (parsed.type === 'nominationChanged') { useUserFlowStore.getState().setCurrentScreen(parsed.data.screenType) void hydrate() return } if (parsed.type === 'showResult') { useUserFlowStore.getState().setCurrentScreen(parsed.data.screenType) useUserFlowStore .getState() .setResultScreenType(mapSseScreenTypeToResultScreenType(parsed.data.screenType)) } }) }, [subscribe, hydrate]) const setCurrentScreen = useCallback((screen: SseScreenType) => { useUserFlowStore.getState().setCurrentScreen(screen) }, []) const routerScreen = resolveRouterScreen({ currentScreen: store.currentScreen, snapshot: store.snapshot, isHydrating: store.isHydrating }) return { currentScreen: store.currentScreen, snapshot: store.snapshot, isHydrating: store.isHydrating, hydrateError: store.hydrateError, sseStatus: status, routerScreen, setCurrentScreen } }