Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 | 'use client' /** * React Query hook for session song status. * * Polls the song API when generation is in progress and listens * for Socket.IO events for instant notification when ready. */ import { useEffect } from 'react' import { useQuery, useQueryClient } from '@tanstack/react-query' import { createSocket } from '@/lib/socket' import { sessionSongKeys } from '@/lib/queryKeys' import { api } from '@/lib/queryClient' import type { SessionSongFailureKind } from '@/db/schema/session-songs' import type { SongLyricsSection } from '@/lib/song/alignment' interface SessionSongData { id: string status: string title: string | null durationSeconds: number | null audioPath: string | null /** URL for the word-alignment JSON. May 404 for legacy songs without timestamps. */ alignmentPath: string | null /** Per-section lyrics for the synced-lyrics player. Null until completed. */ lyrics: SongLyricsSection[] | null triggerSource: string | null failureKind: SessionSongFailureKind | null /** Raw error string — server only includes for owners/admins; null otherwise. */ errorDetail: string | null /** True when the requesting viewer is the player's account owner or an admin. */ viewerIsOwner: boolean createdAt: number | null completedAt: number | null } interface SessionSongResponse { song: SessionSongData | null } interface UseSessionSongOptions { playerId: string planId: string | undefined enabled?: boolean } export function useSessionSong({ playerId, planId, enabled = true }: UseSessionSongOptions) { const queryClient = useQueryClient() const query = useQuery({ queryKey: sessionSongKeys.forPlan(planId ?? ''), queryFn: async (): Promise<SessionSongResponse> => { const res = await api(`curriculum/${playerId}/sessions/plans/${planId}/song`) if (!res.ok) return { song: null } return res.json() as Promise<SessionSongResponse> }, enabled: enabled && !!planId, // Poll every 5s while generating refetchInterval: (query) => { const song = query.state.data?.song if (!song) return false if (song.status === 'completed' || song.status === 'failed') return false return 5000 }, }) // Listen for Socket.IO instant notification useEffect(() => { if (!planId || !enabled) return const socket = createSocket() const eventName = `session-song:ready:${planId}` socket.on(eventName, () => { queryClient.invalidateQueries({ queryKey: sessionSongKeys.forPlan(planId), }) }) return () => { socket.off(eventName) socket.disconnect() } }, [planId, enabled, queryClient]) const song = query.data?.song ?? null const isGenerating = !!song && song.status !== 'completed' && song.status !== 'failed' const isReady = song?.status === 'completed' const hasFailed = song?.status === 'failed' return { song, isGenerating, isReady, hasFailed, failureKind: song?.failureKind ?? null, errorDetail: song?.errorDetail ?? null, viewerIsOwner: song?.viewerIsOwner ?? false, isLoading: query.isLoading, } } |